Skip to content

Commit fc38126

Browse files
committed
Allow explicitly approved, never-sent auto draft to be safely handed to manual publishing
1 parent 7143143 commit fc38126

1 file changed

Lines changed: 64 additions & 2 deletions

File tree

‎src/lib/bob/bob_publish_common.py‎

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,76 @@ def load_record(rid):
8787
return obj if obj.get('run_id')==rid and valid_record(obj) else None
8888
except (OSError,ValueError,TypeError,AttributeError): return None
8989

90+
# A previous auto job can be handed to an explicit manual publish ONLY if
91+
# it demonstrably never reached the network. All ambiguous results stay locked.
92+
AUTO_REVIEW_ONLY_FAILURES=frozenset((
93+
'Human decision already exists',
94+
'Friend-targeted draft requires manual review',
95+
))
96+
97+
98+
def _recoverable_auto_job(db, rid, obj):
99+
row=db.execute(
100+
'SELECT approval_sha256,post_id,draft_comment,mode,state,'
101+
'attempted_at,comment_id,challenge_code,challenge_text,'
102+
'challenge_expires_at,verify_answer,last_error '
103+
'FROM publish_jobs WHERE run_id=?',(rid,)).fetchone()
104+
if not row:
105+
return None
106+
digest,pid,text,mode,state,attempted,comment,code,challenge,expiry,answer,error=row
107+
if (mode!='auto' or digest!=obj['approval_sha256']
108+
or pid!=obj['chosen_post_id'] or text!=obj['draft_comment']
109+
or any(x is not None for x in
110+
(attempted,comment,code,challenge,expiry,answer))):
111+
return None
112+
if (state=='queued' and error in (None,'')
113+
or state=='failed' and error in AUTO_REVIEW_ONLY_FAILURES):
114+
return (state,error)
115+
return None
116+
117+
118+
def manual_recovery_allowed(db,rid):
119+
"""Read-only UI check. The actual handoff revalidates with an atomic CAS."""
120+
obj=load_record(rid)
121+
if not obj:
122+
return False
123+
decision=db.execute(
124+
'SELECT draft_sha256,status FROM decisions WHERE run_id=?',(rid,)).fetchone()
125+
return (decision==(obj['approval_sha256'],'approved')
126+
and _recoverable_auto_job(db,rid,obj) is not None)
127+
128+
90129
def queue_manual(db, rid):
91130
obj=load_record(rid)
92131
if not obj: raise ValueError('Invalid or changed scouting record')
93132
decision=db.execute('SELECT draft_sha256,status FROM decisions WHERE run_id=?',(rid,)).fetchone()
94133
if not decision or decision != (obj['approval_sha256'],'approved'):
95134
raise ValueError('Draft must be explicitly approved and unchanged')
96-
if db.execute('SELECT 1 FROM publish_jobs WHERE run_id=?',(rid,)).fetchone():
97-
raise ValueError('Draft already has a publication attempt; no automatic retry')
135+
existing=db.execute('SELECT 1 FROM publish_jobs WHERE run_id=?',(rid,)).fetchone()
136+
if existing:
137+
recovery=_recoverable_auto_job(db,rid,obj)
138+
if recovery is None:
139+
raise ValueError('Draft already has a publication attempt; no automatic retry')
140+
old_state,old_error=recovery
141+
# The writer and publisher may overlap. Compare the exact original
142+
# job/decision/state and *never* overwrite a sending/uncertain job.
143+
cur=db.execute(
144+
"UPDATE publish_jobs SET mode='manual',state='queued',last_error=NULL,"
145+
"updated_at=? WHERE run_id=? AND mode='auto' AND state=? "
146+
"AND approval_sha256=? AND post_id=? AND draft_comment=? "
147+
"AND attempted_at IS NULL AND comment_id IS NULL "
148+
"AND challenge_code IS NULL AND challenge_text IS NULL "
149+
"AND challenge_expires_at IS NULL AND verify_answer IS NULL "
150+
"AND (state='queued' AND (last_error IS NULL OR last_error='') "
151+
"OR state='failed' AND last_error=?) "
152+
"AND EXISTS(SELECT 1 FROM decisions WHERE run_id=? "
153+
"AND draft_sha256=? AND status='approved')",
154+
(now(),rid,old_state,obj['approval_sha256'],obj['chosen_post_id'],
155+
obj['draft_comment'],old_error,rid,obj['approval_sha256']))
156+
if cur.rowcount!=1:
157+
raise ValueError('Auto job changed; manual handoff not performed')
158+
event(db,'publish_manual_handoff',f'{rid} approved auto draft queued manually; no previous send')
159+
return
98160
db.execute('INSERT INTO publish_jobs(run_id,approval_sha256,post_id,draft_comment,mode,state,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?)',
99161
(rid,obj['approval_sha256'],obj['chosen_post_id'],obj['draft_comment'],'manual','queued',now(),now()))
100162
event(db,'publish_queued',f'{rid} manual publication requested')

0 commit comments

Comments
 (0)