@@ -431,9 +431,9 @@ function writeLegacyTaskStateSidecars(root: string): {
431431 . prepare (
432432 `
433433 INSERT INTO task_runs (
434- task_id, runtime, source_id, requester_session_key, child_session_key, run_id, task ,
435- status, delivery_status, notify_policy, created_at, last_event_at
436- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
434+ task_id, runtime, source_id, requester_session_key, child_session_key, agent_id, run_id ,
435+ task, status, delivery_status, notify_policy, created_at, last_event_at
436+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
437437 ` ,
438438 )
439439 . run (
@@ -442,6 +442,7 @@ function writeLegacyTaskStateSidecars(root: string): {
442442 "nightly" ,
443443 "" ,
444444 "agent:main:cron:nightly" ,
445+ "ops" ,
445446 "legacy-task-run" ,
446447 "Legacy cron task" ,
447448 "running" ,
@@ -507,6 +508,36 @@ function writeLegacyTaskStateSidecars(root: string): {
507508 return { taskRunsPath, flowRunsPath } ;
508509}
509510
511+ function appendLegacyCrossAgentTask ( taskRunsPath : string ) : void {
512+ const sqlite = requireNodeSqlite ( ) ;
513+ const db = new sqlite . DatabaseSync ( taskRunsPath ) ;
514+ try {
515+ db . prepare (
516+ `
517+ INSERT INTO task_runs (
518+ task_id, runtime, requester_session_key, child_session_key, agent_id, run_id, task,
519+ status, delivery_status, notify_policy, created_at, last_event_at
520+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
521+ ` ,
522+ ) . run (
523+ "legacy-cross-agent" ,
524+ "subagent" ,
525+ "agent:main:main" ,
526+ "agent:worker:subagent:child" ,
527+ "main" ,
528+ "legacy-cross-agent-run" ,
529+ "Inspect worker state" ,
530+ "running" ,
531+ "pending" ,
532+ "done_only" ,
533+ 130 ,
534+ 140 ,
535+ ) ;
536+ } finally {
537+ db . close ( ) ;
538+ }
539+ }
540+
510541async function detectAndRunMigrations ( params : {
511542 root : string ;
512543 cfg : OpenClawConfig ;
@@ -2271,6 +2302,7 @@ describe("doctor legacy state migrations", () => {
22712302 ownerKey : "system:cron:nightly" ,
22722303 scopeKind : "system" ,
22732304 requesterSessionKey : "" ,
2305+ agentId : "ops" ,
22742306 runId : "legacy-task-run" ,
22752307 } ) ;
22762308 expect ( taskState . deliveryStates . get ( "legacy-task" ) ) . toMatchObject ( {
@@ -2345,6 +2377,90 @@ describe("doctor legacy state migrations", () => {
23452377 } ) ;
23462378 } ) ;
23472379
2380+ it ( "canonicalizes cross-agent attribution while importing task sidecars" , async ( ) => {
2381+ const root = await makeTempRoot ( ) ;
2382+ const { taskRunsPath } = writeLegacyTaskStateSidecars ( root ) ;
2383+ appendLegacyCrossAgentTask ( taskRunsPath ) ;
2384+
2385+ const result = await autoMigrateLegacyTaskStateSidecars ( {
2386+ env : { OPENCLAW_STATE_DIR : root } as NodeJS . ProcessEnv ,
2387+ } ) ;
2388+
2389+ expect ( result . warnings ) . toStrictEqual ( [ ] ) ;
2390+ expect ( result . changes ) . toContain ( "Migrated 2 task registry sidecar rows → shared SQLite state" ) ;
2391+
2392+ await withStateDir ( root , async ( ) => {
2393+ expect ( loadTaskRegistryStateFromSqlite ( ) . tasks . get ( "legacy-cross-agent" ) ) . toMatchObject ( {
2394+ taskId : "legacy-cross-agent" ,
2395+ agentId : "worker" ,
2396+ requesterAgentId : "main" ,
2397+ requesterSessionKey : "agent:main:main" ,
2398+ childSessionKey : "agent:worker:subagent:child" ,
2399+ } ) ;
2400+ } ) ;
2401+ } ) ;
2402+
2403+ it ( "keeps task sidecars when only requester attribution conflicts" , async ( ) => {
2404+ const root = await makeTempRoot ( ) ;
2405+ const { taskRunsPath } = writeLegacyTaskStateSidecars ( root ) ;
2406+ appendLegacyCrossAgentTask ( taskRunsPath ) ;
2407+
2408+ await withStateDir ( root , async ( ) => {
2409+ loadTaskRegistryStateFromSqlite ( ) ;
2410+ closeOpenClawStateDatabaseForTest ( ) ;
2411+ const sqlite = requireNodeSqlite ( ) ;
2412+ const db = new sqlite . DatabaseSync ( path . join ( root , "state" , "openclaw.sqlite" ) ) ;
2413+ try {
2414+ db . prepare (
2415+ `INSERT INTO task_runs (
2416+ task_id,
2417+ runtime,
2418+ requester_session_key,
2419+ owner_key,
2420+ scope_kind,
2421+ child_session_key,
2422+ agent_id,
2423+ requester_agent_id,
2424+ run_id,
2425+ task,
2426+ status,
2427+ delivery_status,
2428+ notify_policy,
2429+ created_at,
2430+ last_event_at
2431+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
2432+ ) . run (
2433+ "legacy-cross-agent" ,
2434+ "subagent" ,
2435+ "agent:main:main" ,
2436+ "agent:main:main" ,
2437+ "session" ,
2438+ "agent:worker:subagent:child" ,
2439+ "worker" ,
2440+ "other-requester" ,
2441+ "legacy-cross-agent-run" ,
2442+ "Inspect worker state" ,
2443+ "running" ,
2444+ "pending" ,
2445+ "done_only" ,
2446+ 130 ,
2447+ 140 ,
2448+ ) ;
2449+ } finally {
2450+ db . close ( ) ;
2451+ }
2452+ } ) ;
2453+
2454+ const result = await autoMigrateLegacyTaskStateSidecars ( {
2455+ env : { OPENCLAW_STATE_DIR : root } as NodeJS . ProcessEnv ,
2456+ } ) ;
2457+
2458+ expect ( result . warnings ) . toContain (
2459+ "Left task registry sidecar in place because 1 row already existed in shared state: legacy-cross-agent" ,
2460+ ) ;
2461+ expect ( fs . existsSync ( taskRunsPath ) ) . toBe ( true ) ;
2462+ } ) ;
2463+
23482464 it ( "keeps task sidecars when shared state already has conflicting task rows" , async ( ) => {
23492465 const root = await makeTempRoot ( ) ;
23502466 const { taskRunsPath, flowRunsPath } = writeLegacyTaskStateSidecars ( root ) ;
0 commit comments