@@ -16,6 +16,8 @@ export const COORDINATION_TODO_CLAIM_RESULT_SCHEMA =
1616 "loopx_coordination_todo_claim_result_v0" ;
1717export const COORDINATION_TODO_CLAIM_RECEIPT_SCHEMA =
1818 "loopx_coordination_todo_claim_receipt_v0" ;
19+ export const COORDINATION_TODO_CLAIM_DECISION_SCHEMA =
20+ "loopx_coordination_todo_claim_decision_v0" ;
1921
2022export interface CoordinationTodoClaimInput {
2123 readonly goal_id : string ;
@@ -33,6 +35,28 @@ export type CoordinationTodoClaimResult = JsonObject & {
3335 readonly schema_version : typeof COORDINATION_TODO_CLAIM_RESULT_SCHEMA ;
3436} ;
3537
38+ export interface CoordinationTodoClaimAcceptedDecision extends JsonObject {
39+ readonly schema_version : typeof COORDINATION_TODO_CLAIM_DECISION_SCHEMA ;
40+ readonly status : "accepted" ;
41+ readonly owner : string ;
42+ readonly actor : string | null ;
43+ readonly mode : "single_agent_compatibility" | "registered_peer_actor" ;
44+ readonly mutation_authority : JsonObject ;
45+ }
46+
47+ export interface CoordinationTodoClaimRejectedDecision extends JsonObject {
48+ readonly schema_version : typeof COORDINATION_TODO_CLAIM_DECISION_SCHEMA ;
49+ readonly status : "rejected" ;
50+ readonly reason_code : string ;
51+ readonly reason : string ;
52+ }
53+
54+ export type CoordinationTodoClaimDecision =
55+ | CoordinationTodoClaimAcceptedDecision
56+ | CoordinationTodoClaimRejectedDecision ;
57+
58+ type ClaimRejection = CoordinationTodoClaimRejectedDecision | null ;
59+
3660function normalizeAgent ( value : unknown , label : string ) : string {
3761 if ( typeof value !== "string" ) {
3862 throw new AuthorityStoreProtocolError ( `${ label } must be a public-safe agent id` ) ;
@@ -59,85 +83,155 @@ function normalizeRegisteredAgents(value: readonly string[]): string[] {
5983 return normalized ;
6084}
6185
86+ function normalizeExcludedAgents ( value : unknown ) : string [ ] {
87+ if ( value === undefined || value === null ) return [ ] ;
88+ if ( ! Array . isArray ( value ) ) {
89+ throw new AuthorityStoreProtocolError ( "todo.excluded_agents must be an array" ) ;
90+ }
91+ const normalized = value . map ( ( agent , index ) =>
92+ normalizeAgent ( agent , `todo.excluded_agents[${ index } ]` )
93+ ) ;
94+ if ( new Set ( normalized ) . size !== normalized . length ) {
95+ throw new AuthorityStoreProtocolError (
96+ "todo.excluded_agents must contain unique public-safe agent ids" ,
97+ ) ;
98+ }
99+ return normalized ;
100+ }
101+
62102function failure ( code : string , reason : string , detail : JsonObject = { } ) : CoordinationTodoClaimResult {
63103 return {
104+ ...detail ,
64105 schema_version : COORDINATION_TODO_CLAIM_RESULT_SCHEMA ,
65106 status : "failed" ,
66107 reason_code : code ,
67108 reason,
109+ } ;
110+ }
111+
112+ function decisionFailure (
113+ code : string ,
114+ reason : string ,
115+ detail : JsonObject = { } ,
116+ ) : CoordinationTodoClaimRejectedDecision {
117+ return {
68118 ...detail ,
119+ schema_version : COORDINATION_TODO_CLAIM_DECISION_SCHEMA ,
120+ status : "rejected" ,
121+ reason_code : code ,
122+ reason,
69123 } ;
70124}
71125
72- function claimAuthority (
73- todo : JsonObject ,
126+ function rejectInvalidClaimActor (
74127 input : CoordinationTodoClaimInput ,
75- ) : { owner : string ; actor : string | null ; mode : string } | CoordinationTodoClaimResult {
76- const registered = input . registered_agents ;
77- const owner = input . claimed_by ;
78- const actor = input . actor_agent_id ;
128+ ) : ClaimRejection {
129+ const { registered_agents : registered , claimed_by : owner , actor_agent_id : actor } = input ;
79130 if ( ! registered . includes ( owner ) ) {
80- return failure ( "actor_not_registered" , "claimed_by is not registered for this goal" , {
81- claimed_by : owner ,
82- } ) ;
131+ return decisionFailure (
132+ "actor_not_registered" ,
133+ "claimed_by is not registered for this goal" ,
134+ { claimed_by : owner } ,
135+ ) ;
83136 }
84137 if ( actor !== null && ! registered . includes ( actor ) ) {
85- return failure ( "actor_not_registered" , "actor_agent_id is not registered for this goal" , {
86- actor_agent_id : actor ,
87- } ) ;
138+ return decisionFailure (
139+ "actor_not_registered" ,
140+ "actor_agent_id is not registered for this goal" ,
141+ { actor_agent_id : actor } ,
142+ ) ;
88143 }
89144 if ( registered . length > 1 && actor === null ) {
90- return failure ( "actor_required" , "multi-agent Todo claim requires actor_agent_id" ) ;
145+ return decisionFailure (
146+ "actor_required" ,
147+ "multi-agent Todo claim requires actor_agent_id" ,
148+ ) ;
91149 }
92- if ( actor !== null && actor !== owner ) {
93- return failure (
150+ return actor !== null && actor !== owner
151+ ? decisionFailure (
94152 "claim_actor_mismatch" ,
95153 "Todo claim requires claimed_by to match actor_agent_id" ,
96154 { actor_agent_id : actor , claimed_by : owner } ,
97- ) ;
98- }
155+ )
156+ : null ;
157+ }
158+
159+ function rejectIneligibleTodo (
160+ todo : JsonObject ,
161+ input : CoordinationTodoClaimInput ,
162+ ) : ClaimRejection {
99163 if ( todo . role !== "agent" ) {
100- return failure ( "todo_not_agent" , "claimed_by is only valid for agent Todos" ) ;
164+ return decisionFailure ( "todo_not_agent" , "claimed_by is only valid for agent Todos" ) ;
101165 }
102166 if ( input . expected_role !== null && input . expected_role !== todo . role ) {
103- return failure ( "todo_role_mismatch" , "Todo does not have the requested role" , {
104- requested_role : input . expected_role ,
105- todo_role : todo . role ,
106- } ) ;
167+ return decisionFailure (
168+ "todo_role_mismatch" ,
169+ "Todo does not have the requested role" ,
170+ { requested_role : input . expected_role , todo_role : todo . role } ,
171+ ) ;
107172 }
108173 if ( todo . status !== "open" ) {
109- return failure ( "todo_not_open" , "Todo claim requires status=open" , {
110- todo_status : todo . status ,
111- } ) ;
174+ return decisionFailure (
175+ "todo_not_open" ,
176+ "Todo claim requires status=open" ,
177+ { todo_status : todo . status } ,
178+ ) ;
112179 }
113180 if ( todo . archive_state !== "active" ) {
114- return failure ( "todo_archived" , "Todo claim requires an active Todo" ) ;
181+ return decisionFailure ( "todo_archived" , "Todo claim requires an active Todo" ) ;
115182 }
116- if ( typeof todo . removed_continuation_policy === "string" &&
117- todo . removed_continuation_policy . length > 0 ) {
118- return failure (
183+ return typeof todo . removed_continuation_policy === "string" &&
184+ todo . removed_continuation_policy . length > 0
185+ ? decisionFailure (
119186 "removed_continuation_policy" ,
120187 "Todo uses a removed continuation policy and must be repaired before claiming" ,
121- ) ;
122- }
123- const excluded = Array . isArray ( todo . excluded_agents ) ? todo . excluded_agents : [ ] ;
188+ )
189+ : null ;
190+ }
191+
192+ export function evaluateCoordinationTodoClaimDecision (
193+ todo : JsonObject ,
194+ input : CoordinationTodoClaimInput ,
195+ ) : CoordinationTodoClaimDecision {
196+ const registered = input . registered_agents ;
197+ const owner = input . claimed_by ;
198+ const actor = input . actor_agent_id ;
199+ const rejection = rejectInvalidClaimActor ( input ) ?? rejectIneligibleTodo ( todo , input ) ;
200+ if ( rejection !== null ) return rejection ;
201+ const excluded = normalizeExcludedAgents ( todo . excluded_agents ) ;
124202 if ( excluded . includes ( owner ) ) {
125- return failure ( "actor_excluded" , "claiming agent is excluded from this Todo" , {
126- actor_agent_id : owner ,
127- } ) ;
203+ return decisionFailure (
204+ "actor_excluded" ,
205+ "claiming agent is excluded from this Todo" ,
206+ { actor_agent_id : owner } ,
207+ ) ;
128208 }
129209 const existing = typeof todo . claimed_by === "string" && todo . claimed_by . length > 0
130210 ? normalizeAgent ( todo . claimed_by , "todo.claimed_by" )
131211 : null ;
132212 if ( existing !== null && existing !== owner ) {
133- return failure ( "claim_owner_mismatch" , "Todo is already claimed by another agent" , {
134- claim_owner : existing ,
135- } ) ;
213+ return decisionFailure (
214+ "claim_owner_mismatch" ,
215+ "Todo is already claimed by another agent" ,
216+ { claim_owner : existing } ,
217+ ) ;
136218 }
219+ const mode = registered . length <= 1 ? "single_agent_compatibility" : "registered_peer_actor" ;
137220 return {
221+ schema_version : COORDINATION_TODO_CLAIM_DECISION_SCHEMA ,
222+ status : "accepted" ,
138223 owner,
139224 actor,
140- mode : registered . length <= 1 ? "single_agent_compatibility" : "registered_peer_actor" ,
225+ mode,
226+ mutation_authority : {
227+ schema_version : "todo_mutation_authority_v0" ,
228+ command : "claim" ,
229+ mode,
230+ actor_agent_id : actor ,
231+ todo_id : todo . todo_id ,
232+ registered_agent_count : registered . length ,
233+ ...( mode === "registered_peer_actor" ? { claim_owner : existing } : { } ) ,
234+ } ,
141235 } ;
142236}
143237
@@ -269,16 +363,22 @@ export async function executeCoordinationTodoClaim(
269363 } ) ;
270364 }
271365
272- let authority : ReturnType < typeof claimAuthority > ;
366+ let authority : ReturnType < typeof evaluateCoordinationTodoClaimDecision > ;
273367 try {
274- authority = claimAuthority ( todo , input ) ;
368+ authority = evaluateCoordinationTodoClaimDecision ( todo , input ) ;
275369 } catch ( error ) {
276370 return failure (
277371 "invalid_coordination_todo_claim" ,
278372 error instanceof Error ? error . message : "invalid Todo claim" ,
279373 ) ;
280374 }
281- if ( typeof authority . owner !== "string" ) return authority as CoordinationTodoClaimResult ;
375+ if ( authority . status !== "accepted" || typeof authority . owner !== "string" ) {
376+ return failure (
377+ typeof authority . reason_code === "string" ? authority . reason_code : "invalid_coordination_todo_claim" ,
378+ typeof authority . reason === "string" ? authority . reason : "Todo claim was rejected" ,
379+ authority ,
380+ ) ;
381+ }
282382
283383 const handoffMode = typeof head . head . handoff_mode === "string"
284384 ? head . head . handoff_mode
@@ -295,17 +395,10 @@ export async function executeCoordinationTodoClaim(
295395 ) ;
296396 }
297397
298- const mutationAuthority = {
299- schema_version : "todo_mutation_authority_v0" ,
300- command : "claim" ,
301- mode : authority . mode ,
302- actor_agent_id : authority . actor ,
303- todo_id : input . todo_id ,
304- registered_agent_count : input . registered_agents . length ,
305- ...( authority . mode === "registered_peer_actor"
306- ? { claim_owner : typeof todo . claimed_by === "string" ? todo . claimed_by : null }
307- : { } ) ,
308- } ;
398+ const mutationAuthority = canonicalAuthorityObject (
399+ authority . mutation_authority ,
400+ "Todo claim mutation authority" ,
401+ ) ;
309402 const updatedAt = input . now . toISOString ( ) . replace ( / \. \d { 3 } Z $ / u, "Z" ) ;
310403 const changed = todo . claimed_by !== authority . owner ;
311404 const result = {
0 commit comments