@xopcai/xopc 0.0.199 → 0.0.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{activity-store-DYVN1hRp.js → activity-store-DOhnAQOQ.js} +1 -1
  4. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-Bt70bZjv.js → agent-avatar-dicebear-D8Cw0Mve.js} +1 -1
  5. package/dist/gateway/static/root/assets/agents-ANavi648.js +68 -0
  6. package/dist/gateway/static/root/assets/{api-B8mzbAle.js → api-E9Bk-Idp.js} +1 -1
  7. package/dist/gateway/static/root/assets/{app-management-settings-panel-B4x7JfL8.js → app-management-settings-panel-BubrReXg.js} +1 -1
  8. package/dist/gateway/static/root/assets/{appearance-settings-Dwdagf-Q.js → appearance-settings-BY5Az2p4.js} +1 -1
  9. package/dist/gateway/static/root/assets/{apps-page-DkTiesxe.js → apps-page-CFh9MBxc.js} +1 -1
  10. package/dist/gateway/static/root/assets/{archive-plugin-Cv0VXKLm.js → archive-plugin-BY8843mB.js} +1 -1
  11. package/dist/gateway/static/root/assets/{attachment-load-CgidKrXn.js → attachment-load-DG45KScf.js} +1 -1
  12. package/dist/gateway/static/root/assets/automations-page-751CeNI3.js +6 -0
  13. package/dist/gateway/static/root/assets/{binary-plugins-DN3CQgIq.js → binary-plugins-DGIF2ZAv.js} +1 -1
  14. package/dist/gateway/static/root/assets/{block-editor-BwkSl1F_.js → block-editor-C1Qktr1g.js} +5 -5
  15. package/dist/gateway/static/root/assets/browser-settings-page-CGAWs-TT.js +8 -0
  16. package/dist/gateway/static/root/assets/{browser-workflow-inputs-_ndaecsf.js → browser-workflow-inputs-D_jnndyz.js} +1 -1
  17. package/dist/gateway/static/root/assets/{browser-workflows-page-C_I31wLm.js → browser-workflows-page-DyIRvPaq.js} +2 -2
  18. package/dist/gateway/static/root/assets/capabilities-settings-panel-CghUK68J.js +7 -0
  19. package/dist/gateway/static/root/assets/{capability-presets-api-Dy1jZJKs.js → capability-presets-api--tz0mGLY.js} +1 -1
  20. package/dist/gateway/static/root/assets/capability-presets-settings-panel-XsTfkvUn.js +3 -0
  21. package/dist/gateway/static/root/assets/{channel-recipient-api-D-9qiuio.js → channel-recipient-api-CkOKF9nc.js} +1 -1
  22. package/dist/gateway/static/root/assets/{channels-settings-DV5BhAeu.js → channels-settings-DkoAZZMe.js} +1 -1
  23. package/dist/gateway/static/root/assets/channels-status-swr-DNDvMWpl.js +1 -0
  24. package/dist/gateway/static/root/assets/{connectors-api-5FEBYguL.js → connectors-api-FBOe9SWC.js} +1 -1
  25. package/dist/gateway/static/root/assets/connectors-page-e4vekkAu.js +2 -0
  26. package/dist/gateway/static/root/assets/{cron-expression-SEFx5rnr.js → cron-expression-Q4QaYjpI.js} +1 -1
  27. package/dist/gateway/static/root/assets/{date-picker-D4zCUQJL.js → date-picker-Dtn7e6TW.js} +1 -1
  28. package/dist/gateway/static/root/assets/{dependency-picker-DpQAxS2k.js → dependency-picker-Br6nPaM6.js} +1 -1
  29. package/dist/gateway/static/root/assets/{desktop-pet-B_5G3lQl.js → desktop-pet-DzTM6aH8.js} +1 -1
  30. package/dist/gateway/static/root/assets/{desktop-pet-settings-ClEyoUJe.js → desktop-pet-settings-FDmaty3P.js} +1 -1
  31. package/dist/gateway/static/root/assets/{directory-picker-path-field-AsFWmOjG.js → directory-picker-path-field-X2e8xn8j.js} +1 -1
  32. package/dist/gateway/static/root/assets/{extension-debug-page-C22Y2o5l.js → extension-debug-page-Dx4MBfga.js} +1 -1
  33. package/dist/gateway/static/root/assets/{extension-page-C61Ikawk.js → extension-page-Bf_FEDhM.js} +1 -1
  34. package/dist/gateway/static/root/assets/{extension-settings-page-WlWWcDFr.js → extension-settings-page-CrOgZxkf.js} +1 -1
  35. package/dist/gateway/static/root/assets/{gateway-config-api-ByIOMQdc.js → gateway-config-api-CZ4r-7qT.js} +1 -1
  36. package/dist/gateway/static/root/assets/{gateway-config-swr-jB0hAhWO.js → gateway-config-swr-Cv2ywpdb.js} +1 -1
  37. package/dist/gateway/static/root/assets/{gateway-settings-CzzS9A17.js → gateway-settings-DpWmVi0w.js} +1 -1
  38. package/dist/gateway/static/root/assets/{gateway-startup-retry-C-fRlHCL.js → gateway-startup-retry-undf-k5g.js} +1 -1
  39. package/dist/gateway/static/root/assets/heartbeat-settings-DSQZhSvT.js +1 -0
  40. package/dist/gateway/static/root/assets/home-page-L0uHiu0T.js +3 -0
  41. package/dist/gateway/static/root/assets/{index-CR233S0Q.css → index-Bp8I_eX4.css} +1 -1
  42. package/dist/gateway/static/root/assets/index-DRrjYYzZ.js +152 -0
  43. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BD_TFz0_.js → keyboard-shortcuts-settings-Bc3Quw42.js} +1 -1
  44. package/dist/gateway/static/root/assets/local-app-workbench-page-Copi3sdP.js +2 -0
  45. package/dist/gateway/static/root/assets/{local-apps-page-BwbUeLfm.js → local-apps-page-CP9X_vtz.js} +1 -1
  46. package/dist/gateway/static/root/assets/{locale-store-xULEcFVs.js → locale-store-CnamlR07.js} +1 -1
  47. package/dist/gateway/static/root/assets/logs-page-DMKq6e29.js +2 -0
  48. package/dist/gateway/static/root/assets/{management-settings-CW2VpqtT.js → management-settings-CJmcaVje.js} +1 -1
  49. package/dist/gateway/static/root/assets/{markdown-split-B1M9KO16.js → markdown-split-AcuFp0TH.js} +1 -1
  50. package/dist/gateway/static/root/assets/{media-plugins-B_QlNdqZ.js → media-plugins-BvFgsiJL.js} +1 -1
  51. package/dist/gateway/static/root/assets/messages-D7kOD_o_.js +3 -0
  52. package/dist/gateway/static/root/assets/{notes-page-BTbD4GmH.js → notes-page-IW7IQ9nJ.js} +1 -1
  53. package/dist/gateway/static/root/assets/notes-workbench-Zb8U0anO.js +4 -0
  54. package/dist/gateway/static/root/assets/{preview-open-alternatives-CeqtArlZ.js → preview-open-alternatives-C-b34dgQ.js} +1 -1
  55. package/dist/gateway/static/root/assets/{product-open-page-DnalBZZs.js → product-open-page-CWEzLykF.js} +1 -1
  56. package/dist/gateway/static/root/assets/project-detail-page-BuIbLnan.js +1 -0
  57. package/dist/gateway/static/root/assets/projects-page-BwUfRqNV.js +1 -0
  58. package/dist/gateway/static/root/assets/remote-access-hub-BTaXxcEi.js +1 -0
  59. package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-cGkBEfg4.js → runtime-tools-settings-panel-DAohEZrX.js} +2 -2
  60. package/dist/gateway/static/root/assets/{schema-form-CmsrLlxu.js → schema-form-C_OBW8A5.js} +1 -1
  61. package/dist/gateway/static/root/assets/sessions-page-BnHoTCD3.js +1 -0
  62. package/dist/gateway/static/root/assets/settings-advanced-gate-BTB_fxjt.js +1 -0
  63. package/dist/gateway/static/root/assets/{settings-form-section-DUWCbaMJ.js → settings-form-section-C7w2WS-A.js} +1 -1
  64. package/dist/gateway/static/root/assets/{settings-loading-skeleton-Bvy5rdMY.js → settings-loading-skeleton-zwibve4m.js} +1 -1
  65. package/dist/gateway/static/root/assets/{settings-page-cZSAwsRR.js → settings-page-B9W03dSW.js} +1 -1
  66. package/dist/gateway/static/root/assets/setup-status-panel-C8Hec2Af.js +1 -0
  67. package/dist/gateway/static/root/assets/{share-preview-page-BzA2NCAH.js → share-preview-page-6e3ks2be.js} +1 -1
  68. package/dist/gateway/static/root/assets/shares-settings-Bwc5HfLI.js +1 -0
  69. package/dist/gateway/static/root/assets/skill-reload-api-D7kunhqy.js +1 -0
  70. package/dist/gateway/static/root/assets/{skills-page-Bvde7_i5.js → skills-page-WgkaNzpc.js} +1 -1
  71. package/dist/gateway/static/root/assets/{skills-page.utils-vXSqHR7g.js → skills-page.utils-D9QT5cxj.js} +1 -1
  72. package/dist/gateway/static/root/assets/{system-settings-panel-9cQGOGCK.js → system-settings-panel-c1HVtv1Y.js} +1 -1
  73. package/dist/gateway/static/root/assets/task-detail-page-CjP-6WVU.js +1 -0
  74. package/dist/gateway/static/root/assets/{theme-store-CHQRiuCF.js → theme-store-BDyUTObE.js} +1 -1
  75. package/dist/gateway/static/root/assets/{use-autosave-7XfKULPI.js → use-autosave-D6UkH_Ga.js} +1 -1
  76. package/dist/gateway/static/root/assets/{user-context-page-CZ9m9aC_.js → user-context-page-D8ZhPDNF.js} +1 -1
  77. package/dist/gateway/static/root/assets/{voice-api-key-field-DXr21v-3.js → voice-api-key-field-BOxkKbGh.js} +1 -1
  78. package/dist/gateway/static/root/assets/{work-discovery-overlay-CeG7g_DZ.js → work-discovery-overlay-Dd19yK9t.js} +1 -1
  79. package/dist/gateway/static/root/assets/work-discovery-page-DUkdJl1e.js +1 -0
  80. package/dist/gateway/static/root/assets/{workflow-run-setup-panel-DSQQZP9T.js → workflow-run-setup-panel-ZA3bP65I.js} +2 -2
  81. package/dist/gateway/static/root/assets/workflows-page-C2b5W5bw.js +5 -0
  82. package/dist/gateway/static/root/assets/{working-directory-picker-modal-D-IgcHPp.js → working-directory-picker-modal-BcyiDXU3.js} +1 -1
  83. package/dist/gateway/static/root/index.html +9 -9
  84. package/dist/package.js +1 -1
  85. package/dist/packages/gateway-contract/src/tasks.js +51 -9
  86. package/dist/src/agent/context/execution-scope.js +3 -1
  87. package/dist/src/agent/tasks/task-judge-service.d.ts +1 -1
  88. package/dist/src/agent/tasks/task-judge-service.js +3 -2
  89. package/dist/src/agent/tools/xopc-use-tool.js +13 -3
  90. package/dist/src/auth/oauth/types.d.ts +2 -0
  91. package/dist/src/auth/oauth/xopc-cloud.js +24 -2
  92. package/dist/src/automations/domain/validation.d.ts +0 -12
  93. package/dist/src/automations/draft/automation-draft-validator.d.ts +0 -3
  94. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  95. package/dist/src/gateway/hono/app.js +1 -1
  96. package/dist/src/gateway/hono/oauth-async.js +2 -0
  97. package/dist/src/gateway/hono/routes/agent-stream.js +2 -74
  98. package/dist/src/gateway/hono/routes/session-input-handler.d.ts +62 -0
  99. package/dist/src/gateway/hono/routes/session-input-handler.js +82 -0
  100. package/dist/src/gateway/hono/routes/tasks.js +161 -2
  101. package/dist/src/gateway/service/task-conversation-query-service.d.ts +58 -0
  102. package/dist/src/gateway/service/task-conversation-query-service.js +84 -0
  103. package/dist/src/gateway/service.d.ts +10 -0
  104. package/dist/src/gateway/service.js +75 -42
  105. package/dist/src/projects/project-agent.js +1 -1
  106. package/dist/src/storage/sqlite/migrations/120_task_conversation.sql +112 -0
  107. package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
  108. package/dist/src/storage/sqlite/migrations/runner.js +3 -3
  109. package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
  110. package/dist/src/storage/sqlite/schema.js +1 -1
  111. package/dist/src/tasks/execution-context.js +8 -4
  112. package/dist/src/tasks/index.d.ts +2 -0
  113. package/dist/src/tasks/index.js +3 -1
  114. package/dist/src/tasks/task-application-service.js +10 -17
  115. package/dist/src/tasks/task-context-assembler.js +9 -4
  116. package/dist/src/tasks/task-context-repository.d.ts +0 -6
  117. package/dist/src/tasks/task-context-repository.js +0 -4
  118. package/dist/src/tasks/task-conversation-repository.d.ts +71 -0
  119. package/dist/src/tasks/task-conversation-repository.js +194 -0
  120. package/dist/src/tasks/task-handoff-service.d.ts +31 -0
  121. package/dist/src/tasks/task-handoff-service.js +143 -0
  122. package/dist/src/tasks/task-read-model-projector.js +1 -5
  123. package/dist/src/tasks/task-repository.d.ts +0 -2
  124. package/dist/src/tasks/task-repository.js +2 -9
  125. package/dist/src/tasks/task-run-coordinator.js +3 -1
  126. package/dist/src/workflows/service/workflow-run-service.js +4 -2
  127. package/package.json +2 -2
  128. package/dist/gateway/static/root/assets/agents-D19uNmJf.js +0 -68
  129. package/dist/gateway/static/root/assets/automations-page-BR6qMOVr.js +0 -6
  130. package/dist/gateway/static/root/assets/browser-settings-page-CDFQWAxI.js +0 -8
  131. package/dist/gateway/static/root/assets/capabilities-settings-panel-D41mssOq.js +0 -7
  132. package/dist/gateway/static/root/assets/capability-presets-settings-panel-BXgCyuu2.js +0 -3
  133. package/dist/gateway/static/root/assets/channels-status-swr-B4Rbh3bM.js +0 -1
  134. package/dist/gateway/static/root/assets/connectors-page-C_qkEN0T.js +0 -2
  135. package/dist/gateway/static/root/assets/heartbeat-settings-B4A_CXUL.js +0 -1
  136. package/dist/gateway/static/root/assets/home-page-FVGYYPWo.js +0 -3
  137. package/dist/gateway/static/root/assets/index-CGbKTCST.js +0 -152
  138. package/dist/gateway/static/root/assets/local-app-workbench-page-BNpkigNb.js +0 -2
  139. package/dist/gateway/static/root/assets/logs-page-DXP9OQQt.js +0 -2
  140. package/dist/gateway/static/root/assets/messages-3qRsLZuX.js +0 -3
  141. package/dist/gateway/static/root/assets/notes-workbench-JRiq7QDW.js +0 -4
  142. package/dist/gateway/static/root/assets/project-detail-page-CjWA13aE.js +0 -1
  143. package/dist/gateway/static/root/assets/projects-page-jsCkScbG.js +0 -1
  144. package/dist/gateway/static/root/assets/remote-access-hub-CGPJTEkM.js +0 -1
  145. package/dist/gateway/static/root/assets/sessions-page-BBHzziMd.js +0 -1
  146. package/dist/gateway/static/root/assets/settings-advanced-gate-2cGg8-9h.js +0 -1
  147. package/dist/gateway/static/root/assets/setup-status-panel-Cf06b66-.js +0 -1
  148. package/dist/gateway/static/root/assets/shares-settings-xlNKydYY.js +0 -1
  149. package/dist/gateway/static/root/assets/skill-reload-api-C5Xu93c_.js +0 -1
  150. package/dist/gateway/static/root/assets/task-detail-page-Bkno4tTe.js +0 -1
  151. package/dist/gateway/static/root/assets/work-discovery-page-CQWZD03N.js +0 -1
  152. package/dist/gateway/static/root/assets/workflows-page-Cs-MXFJ1.js +0 -5
@@ -0,0 +1,112 @@
1
+ ALTER TABLE task_sessions ADD COLUMN agent_id TEXT;
2
+ ALTER TABLE task_sessions ADD COLUMN run_id TEXT;
3
+ ALTER TABLE task_sessions ADD COLUMN assignment_epoch INTEGER NOT NULL DEFAULT 0;
4
+ ALTER TABLE task_sessions ADD COLUMN status TEXT NOT NULL DEFAULT 'completed'
5
+ CHECK (status IN ('active', 'completed', 'superseded', 'failed'));
6
+ ALTER TABLE task_sessions ADD COLUMN started_at INTEGER;
7
+ ALTER TABLE task_sessions ADD COLUMN ended_at INTEGER;
8
+
9
+ UPDATE task_sessions
10
+ SET agent_id = (
11
+ SELECT sessions.agent_id FROM sessions WHERE sessions.session_key = task_sessions.session_key
12
+ ), started_at = created_at;
13
+
14
+ UPDATE task_sessions
15
+ SET run_id = (
16
+ SELECT task_runs.run_id
17
+ FROM task_runs
18
+ WHERE task_runs.task_id = task_sessions.task_id
19
+ AND task_runs.session_key = task_sessions.session_key
20
+ ORDER BY task_runs.queued_at DESC
21
+ LIMIT 1
22
+ )
23
+ WHERE role = 'execution';
24
+
25
+ UPDATE task_sessions
26
+ SET status = 'superseded', ended_at = created_at
27
+ WHERE role = 'execution';
28
+
29
+ DELETE FROM task_sessions
30
+ WHERE role = 'execution'
31
+ AND session_key IS NOT NULL
32
+ AND task_session_id NOT IN (
33
+ SELECT canonical.task_session_id
34
+ FROM task_sessions AS canonical
35
+ WHERE canonical.role = 'execution'
36
+ AND canonical.session_key IS NOT NULL
37
+ AND canonical.task_session_id = (
38
+ SELECT candidate.task_session_id
39
+ FROM task_sessions AS candidate
40
+ WHERE candidate.role = 'execution'
41
+ AND candidate.session_key = canonical.session_key
42
+ ORDER BY candidate.created_at DESC, candidate.task_session_id DESC
43
+ LIMIT 1
44
+ )
45
+ );
46
+
47
+ UPDATE task_sessions
48
+ SET status = 'active', ended_at = NULL, assignment_epoch = 1
49
+ WHERE task_session_id IN (
50
+ SELECT latest.task_session_id FROM task_sessions AS latest
51
+ WHERE latest.role = 'execution'
52
+ AND latest.task_session_id = (
53
+ SELECT candidate.task_session_id
54
+ FROM task_sessions AS candidate
55
+ WHERE candidate.task_id = latest.task_id AND candidate.role = 'execution'
56
+ ORDER BY candidate.created_at DESC, candidate.task_session_id DESC
57
+ LIMIT 1
58
+ )
59
+ );
60
+
61
+ CREATE UNIQUE INDEX idx_task_sessions_active_execution
62
+ ON task_sessions(task_id)
63
+ WHERE role = 'execution' AND status = 'active';
64
+ CREATE UNIQUE INDEX idx_task_sessions_execution_owner
65
+ ON task_sessions(session_key)
66
+ WHERE role = 'execution' AND session_key IS NOT NULL;
67
+
68
+ CREATE TABLE task_conversation_state (
69
+ task_id TEXT PRIMARY KEY,
70
+ active_session_key TEXT,
71
+ current_executor_agent_id TEXT,
72
+ assignment_epoch INTEGER NOT NULL DEFAULT 0,
73
+ status TEXT NOT NULL DEFAULT 'idle'
74
+ CHECK (status IN ('idle', 'active')),
75
+ updated_at INTEGER NOT NULL,
76
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE,
77
+ FOREIGN KEY (active_session_key) REFERENCES sessions(session_key) ON DELETE SET NULL
78
+ );
79
+
80
+ INSERT INTO task_conversation_state (
81
+ task_id, active_session_key, current_executor_agent_id,
82
+ assignment_epoch, status, updated_at
83
+ )
84
+ SELECT
85
+ tasks.task_id,
86
+ active.session_key,
87
+ COALESCE(active.agent_id, tasks.delegate_agent_id, tasks.owner_id),
88
+ COALESCE(active.assignment_epoch, 0),
89
+ CASE WHEN active.session_key IS NULL THEN 'idle' ELSE 'active' END,
90
+ tasks.updated_at
91
+ FROM tasks
92
+ LEFT JOIN task_sessions AS active
93
+ ON active.task_id = tasks.task_id
94
+ AND active.role = 'execution'
95
+ AND active.status = 'active';
96
+
97
+ CREATE TABLE task_handoff_snapshots (
98
+ snapshot_id TEXT PRIMARY KEY,
99
+ task_id TEXT NOT NULL,
100
+ from_session_key TEXT,
101
+ to_session_key TEXT NOT NULL,
102
+ from_agent_id TEXT,
103
+ to_agent_id TEXT NOT NULL,
104
+ assignment_epoch INTEGER NOT NULL,
105
+ payload_json TEXT NOT NULL,
106
+ created_at INTEGER NOT NULL,
107
+ FOREIGN KEY (task_id) REFERENCES tasks(task_id) ON DELETE CASCADE,
108
+ FOREIGN KEY (from_session_key) REFERENCES sessions(session_key) ON DELETE SET NULL,
109
+ FOREIGN KEY (to_session_key) REFERENCES sessions(session_key) ON DELETE CASCADE
110
+ );
111
+ CREATE INDEX idx_task_handoffs_task
112
+ ON task_handoff_snapshots(task_id, assignment_epoch DESC);
@@ -3,7 +3,7 @@ import type { ApplyMigrationsOptions } from './types.js';
3
3
  /** Baseline schema version applied from schema.sql on first open. */
4
4
  export declare const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
5
5
  /** Latest schema version this release supports (increment when adding migrations). */
6
- export declare const XOPC_DB_SCHEMA_VERSION = 119;
6
+ export declare const XOPC_DB_SCHEMA_VERSION = 120;
7
7
  export declare function resolveMigrationsDir(override?: string): string;
8
8
  /**
9
9
  * Apply sequential SQL migrations from (currentVersion + 1) through targetVersion.
@@ -12,7 +12,7 @@ const log = createLogger("Sqlite:Migrations");
12
12
  /** Baseline schema version applied from schema.sql on first open. */
13
13
  const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
14
14
  /** Latest schema version this release supports (increment when adding migrations). */
15
- const XOPC_DB_SCHEMA_VERSION = 119;
15
+ const XOPC_DB_SCHEMA_VERSION = 120;
16
16
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
17
17
  function backupBeforeTaskCutover(db, databasePath) {
18
18
  db.exec("PRAGMA wal_checkpoint(FULL)");
@@ -57,7 +57,7 @@ function applySingleMigration(db, migration) {
57
57
  * Each step runs in its own transaction and updates schema_meta on success.
58
58
  */
59
59
  function applyPendingMigrations(db, options = {}) {
60
- const targetVersion = options.targetVersion ?? 119;
60
+ const targetVersion = options.targetVersion ?? 120;
61
61
  let currentVersion = readSchemaVersion(db);
62
62
  if (currentVersion > targetVersion) throw new DatabaseSchemaTooNewError(currentVersion, targetVersion);
63
63
  if (currentVersion >= targetVersion) return currentVersion;
@@ -94,7 +94,7 @@ function applyPendingMigrations(db, options = {}) {
94
94
  }
95
95
  /** Inspect schema version without mutating the database. */
96
96
  function inspectSchemaMigrationStatus(db, options = {}) {
97
- const appVersion = options.targetVersion ?? 119;
97
+ const appVersion = options.targetVersion ?? 120;
98
98
  const dbVersion = readSchemaVersion(db);
99
99
  if (dbVersion > appVersion) return {
100
100
  dbVersion,
@@ -19,7 +19,7 @@ const log = createLogger('Sqlite:Migrations');
19
19
  export const XOPC_DB_BASELINE_SCHEMA_VERSION = 11;
20
20
 
21
21
  /** Latest schema version this release supports (increment when adding migrations). */
22
- export const XOPC_DB_SCHEMA_VERSION = 119;
22
+ export const XOPC_DB_SCHEMA_VERSION = 120;
23
23
 
24
24
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
25
25
 
@@ -32,7 +32,7 @@ function ensureXopcDatabaseSchema(db, options = {}) {
32
32
  currentVersion = 11;
33
33
  }
34
34
  applyPendingMigrations(db, {
35
- targetVersion: 119,
35
+ targetVersion: 120,
36
36
  databasePath: options.databasePath
37
37
  });
38
38
  }
@@ -1,3 +1,6 @@
1
+ import { isXopcDatabaseOpen } from "../storage/sqlite/connection.js";
2
+ import "../storage/sqlite/index.js";
3
+ import { TaskConversationRepository } from "./task-conversation-repository.js";
1
4
  //#region src/tasks/execution-context.ts
2
5
  const ORIGINS = new Set([
3
6
  "chat",
@@ -17,10 +20,10 @@ const TRIGGERS = new Set([
17
20
  function optionalString(value) {
18
21
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
19
22
  }
20
- function metadataOrigin(metadata) {
23
+ function metadataOrigin(metadata, taskId) {
21
24
  const explicit = optionalString(metadata.customData?.origin);
22
25
  if (explicit && ORIGINS.has(explicit)) return explicit;
23
- if (optionalString(metadata.customData?.taskId)) return "task";
26
+ if (taskId) return "task";
24
27
  if (metadata.sessionType === "workflow-run" || metadata.sessionType === "workflow-subagent") return "workflow";
25
28
  if (metadata.sessionType === "cron") return "automation";
26
29
  if (metadata.sessionType === "heartbeat") return "proactive";
@@ -35,14 +38,15 @@ function metadataTrigger(metadata) {
35
38
  return "user";
36
39
  }
37
40
  function resolveExecutionContext(input) {
41
+ const taskId = isXopcDatabaseOpen() ? new TaskConversationRepository().resolveActiveExecutionSession(input.sessionKey)?.taskId : void 0;
38
42
  return {
39
43
  runId: input.runId,
40
44
  sessionKey: input.sessionKey,
41
45
  channel: input.channel,
42
46
  agentId: input.agentId,
43
47
  projectId: input.metadata.projectId,
44
- taskId: optionalString(input.metadata.customData?.taskId),
45
- origin: metadataOrigin(input.metadata),
48
+ taskId,
49
+ origin: metadataOrigin(input.metadata, taskId),
46
50
  triggerKind: metadataTrigger(input.metadata),
47
51
  parentRunId: optionalString(input.metadata.customData?.parentRunId),
48
52
  contextTraceId: optionalString(input.metadata.customData?.contextTraceId),
@@ -13,6 +13,8 @@ export * from './task-value-metrics-service.js';
13
13
  export * from './task-application-service.js';
14
14
  export * from './task-change-events.js';
15
15
  export * from './task-context-repository.js';
16
+ export * from './task-conversation-repository.js';
17
+ export * from './task-handoff-service.js';
16
18
  export * from './task-dependency-service.js';
17
19
  export * from './task-read-model-projector.js';
18
20
  export * from './task-outbox-dispatcher.js';
@@ -1,4 +1,5 @@
1
1
  import { TaskRepository } from "./task-repository.js";
2
+ import { TaskConversationConflictError, TaskConversationRepository } from "./task-conversation-repository.js";
2
3
  import { TaskRunRepository } from "./task-run-repository.js";
3
4
  import { TaskReadModelProjector } from "./task-read-model-projector.js";
4
5
  import { resolveExecutionContext } from "./execution-context.js";
@@ -16,7 +17,8 @@ import { TaskRunCoordinator } from "./task-run-coordinator.js";
16
17
  import { ProjectMonitoringService, decideProactiveDisposition, isInQuietHours, nextQuietHoursEnd, proactiveDispositionReason } from "./project-monitoring-service.js";
17
18
  import { ProjectOperatingViewService, summarizeProjectOperatingView } from "./project-operating-view-service.js";
18
19
  import { TaskValueMetricsService } from "./task-value-metrics-service.js";
20
+ import { TaskHandoffService } from "./task-handoff-service.js";
19
21
  import { TaskOutboxDispatcher } from "./task-outbox-dispatcher.js";
20
22
  import { TaskRunDispatcher } from "./task-run-dispatcher.js";
21
23
  import { TaskSignalService } from "./task-signal-service.js";
22
- export { AttentionGovernor, HomeQueryService, ModelTaskContractPlanner, ProjectMonitoringService, ProjectOperatingViewService, TaskApplicationService, TaskContextRepository, TaskDependencyError, TaskDependencyService, TaskOutboxDispatcher, TaskReadModelProjector, TaskRepository, TaskRunCoordinator, TaskRunDispatcher, TaskRunRepository, TaskSignalService, TaskValueMetricsService, TaskWorkflowCoordinator, assembleTaskContext, buildHomeBriefing, buildHomeFocusItems, buildTaskExecutionDirective, decideProactiveDisposition, decisionFromTask, defineTaskContract, enqueueTaskChanged, enqueueTaskChangedEvent, getTaskContextManifest, getTaskExecutionBrief, isInQuietHours, nextQuietHoursEnd, parseTaskContractResponse, proactiveDispositionReason, resolveExecutionContext, summarizeProjectOperatingView };
24
+ export { AttentionGovernor, HomeQueryService, ModelTaskContractPlanner, ProjectMonitoringService, ProjectOperatingViewService, TaskApplicationService, TaskContextRepository, TaskConversationConflictError, TaskConversationRepository, TaskDependencyError, TaskDependencyService, TaskHandoffService, TaskOutboxDispatcher, TaskReadModelProjector, TaskRepository, TaskRunCoordinator, TaskRunDispatcher, TaskRunRepository, TaskSignalService, TaskValueMetricsService, TaskWorkflowCoordinator, assembleTaskContext, buildHomeBriefing, buildHomeFocusItems, buildTaskExecutionDirective, decideProactiveDisposition, decisionFromTask, defineTaskContract, enqueueTaskChanged, enqueueTaskChangedEvent, getTaskContextManifest, getTaskExecutionBrief, isInQuietHours, nextQuietHoursEnd, parseTaskContractResponse, proactiveDispositionReason, resolveExecutionContext, summarizeProjectOperatingView };
@@ -43,7 +43,6 @@ function commandChangedFields(command) {
43
43
  ];
44
44
  case "add_wait":
45
45
  case "resolve_wait": return ["runs", "attention"];
46
- case "delegate": return ["delegateAgentId"];
47
46
  case "revise_contract": return ["contract"];
48
47
  }
49
48
  }
@@ -81,6 +80,10 @@ var TaskApplicationService = class {
81
80
  ...input.contract,
82
81
  createdBy: actor
83
82
  });
83
+ db.prepare(`INSERT INTO task_conversation_state (
84
+ task_id, active_session_key, current_executor_agent_id,
85
+ assignment_epoch, status, updated_at
86
+ ) VALUES (?, NULL, ?, 0, 'idle', ?)`).run(task.id, task.delegateAgentId ?? task.ownerId ?? null, task.createdAt);
84
87
  let current = task;
85
88
  if (input.dependencies.length > 0) current = this.#dependencies.replace({
86
89
  taskId: task.id,
@@ -128,7 +131,12 @@ var TaskApplicationService = class {
128
131
  ok: true,
129
132
  model: this.#projector.project(current)
130
133
  };
131
- const started = this.start(current.id, current.version, input.activation.executor, {
134
+ const executor = input.activation.executor ?? (current.delegateAgentId ? {
135
+ kind: "agent",
136
+ agentId: current.delegateAgentId
137
+ } : void 0);
138
+ if (!executor) throw new Error("Task start executor was not resolved");
139
+ const started = this.start(current.id, current.version, executor, {
132
140
  idempotencyKey: `${input.idempotencyKey}:start`,
133
141
  scheduleAt: input.activation.scheduleAt,
134
142
  actor,
@@ -248,21 +256,6 @@ var TaskApplicationService = class {
248
256
  }
249
257
  break;
250
258
  }
251
- case "delegate": {
252
- const updated = this.#tasks.update(task.id, {
253
- expectedVersion: task.version,
254
- delegateAgentId: input.command.agentId ?? null
255
- });
256
- result = updated ? {
257
- ok: true,
258
- model: this.#projector.project(updated)
259
- } : {
260
- ok: false,
261
- reason: "conflict",
262
- model: this.#projector.get(task.id)
263
- };
264
- break;
265
- }
266
259
  case "revise_contract": {
267
260
  const updated = this.#tasks.reviseContract({
268
261
  taskId: task.id,
@@ -1,7 +1,7 @@
1
1
  import { isXopcDatabaseOpen } from "../storage/sqlite/connection.js";
2
- import { getSessionMetadata } from "../storage/sqlite/session-repository.js";
3
2
  import "../storage/sqlite/index.js";
4
3
  import { TaskRepository } from "./task-repository.js";
4
+ import { TaskConversationRepository } from "./task-conversation-repository.js";
5
5
  import { TaskRunRepository } from "./task-run-repository.js";
6
6
  import { TaskContextRepository } from "./task-context-repository.js";
7
7
  //#region src/tasks/task-context-assembler.ts
@@ -12,8 +12,7 @@ const STANDARD = {
12
12
  reason: "No active task requires expanded context."
13
13
  };
14
14
  function taskIdForSession(sessionKey) {
15
- const value = getSessionMetadata(sessionKey)?.customData?.taskId;
16
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
15
+ return new TaskConversationRepository().resolveActiveExecutionSession(sessionKey)?.taskId;
17
16
  }
18
17
  function getTaskExecutionBrief(taskId) {
19
18
  if (!isXopcDatabaseOpen()) return void 0;
@@ -87,6 +86,8 @@ function assembleTaskContext(sessionKey, userQuery) {
87
86
  allocation: STANDARD
88
87
  };
89
88
  const { task, latestReceipt, remainingCriteria, allocation, manifest } = brief;
89
+ const handoff = new TaskConversationRepository().getLatestHandoff(taskId);
90
+ const handoffPayload = handoff?.toSessionKey === sessionKey ? JSON.stringify(handoff.payload).slice(0, 12e3) : "";
90
91
  const contract = task.contract;
91
92
  return {
92
93
  taskId,
@@ -97,7 +98,8 @@ function assembleTaskContext(sessionKey, userQuery) {
97
98
  remainingCriteria.length ? `Remaining acceptance criteria: ${remainingCriteria.join("; ")}` : "",
98
99
  contract.constraints.length ? `Constraints: ${contract.constraints.join("; ")}` : "",
99
100
  contract.risks.length ? `Risks: ${contract.risks.join("; ")}` : "",
100
- latestReceipt?.summary ? `Latest run: ${latestReceipt.summary}` : ""
101
+ latestReceipt?.summary ? `Latest run: ${latestReceipt.summary}` : "",
102
+ handoffPayload ? `Handoff snapshot: ${handoffPayload}` : ""
101
103
  ].filter(Boolean).join("\n"),
102
104
  allocation,
103
105
  manifest
@@ -108,6 +110,8 @@ function buildTaskExecutionDirective(sessionKey) {
108
110
  const brief = taskId ? getTaskExecutionBrief(taskId) : void 0;
109
111
  if (!brief) return "";
110
112
  const { task, remainingCriteria } = brief;
113
+ const handoff = new TaskConversationRepository().getLatestHandoff(task.id);
114
+ const handoffPayload = handoff?.toSessionKey === sessionKey ? JSON.stringify(handoff.payload).slice(0, 12e3) : "";
111
115
  const contract = task.contract;
112
116
  return [
113
117
  "<xopc_task_execution>",
@@ -117,6 +121,7 @@ function buildTaskExecutionDirective(sessionKey) {
117
121
  `Remaining acceptance criteria: ${remainingCriteria.join("; ")}`,
118
122
  contract.constraints.length ? `Constraints: ${contract.constraints.join("; ")}` : "",
119
123
  contract.approvalRequired.length ? `Authority required: ${contract.approvalRequired.join("; ")}` : "",
124
+ handoffPayload ? `Handoff snapshot: ${handoffPayload}` : "",
120
125
  "Take safe in-scope steps and produce inspectable evidence. Do not claim completion without verification.",
121
126
  "</xopc_task_execution>"
122
127
  ].filter(Boolean).join("\n");
@@ -42,11 +42,5 @@ export declare class TaskContextRepository {
42
42
  }): TaskAuthorityGrant;
43
43
  listActiveGrants(taskId: string, now?: number): TaskAuthorityGrant[];
44
44
  revoke(grantId: string, now?: number): boolean;
45
- linkSession(input: {
46
- taskId: string;
47
- sessionKey: string;
48
- role: 'primary' | 'discussion' | 'execution';
49
- now?: number;
50
- }): void;
51
45
  private requireGrant;
52
46
  }
@@ -82,10 +82,6 @@ var TaskContextRepository = class {
82
82
  revoke(grantId, now = Date.now()) {
83
83
  return getSqliteDatabase().prepare(`UPDATE task_authority_grants SET revoked_at = ?
84
84
  WHERE grant_id = ? AND revoked_at IS NULL`).run(now, grantId).changes > 0;
85
- }
86
- linkSession(input) {
87
- getSqliteDatabase().prepare(`INSERT OR IGNORE INTO task_sessions (task_session_id, task_id, session_key, role, created_at)
88
- VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), input.taskId, input.sessionKey, input.role, input.now ?? Date.now());
89
85
  }
90
86
  requireGrant(id) {
91
87
  const row = getSqliteDatabase().prepare("SELECT * FROM task_authority_grants WHERE grant_id = ?").get(id);
@@ -0,0 +1,71 @@
1
+ export type TaskConversationStatus = 'idle' | 'active';
2
+ export type TaskSessionRole = 'primary' | 'discussion' | 'execution';
3
+ export type TaskSessionStatus = 'active' | 'completed' | 'superseded' | 'failed';
4
+ export interface TaskConversationState {
5
+ taskId: string;
6
+ activeSessionKey?: string;
7
+ currentExecutorAgentId?: string;
8
+ assignmentEpoch: number;
9
+ status: TaskConversationStatus;
10
+ updatedAt: number;
11
+ }
12
+ export interface TaskSessionLink {
13
+ id: string;
14
+ taskId: string;
15
+ sessionKey: string;
16
+ role: TaskSessionRole;
17
+ agentId?: string;
18
+ runId?: string;
19
+ assignmentEpoch: number;
20
+ status: TaskSessionStatus;
21
+ startedAt: number;
22
+ endedAt?: number;
23
+ createdAt: number;
24
+ }
25
+ export interface TaskHandoffSnapshot {
26
+ id: string;
27
+ taskId: string;
28
+ fromSessionKey?: string;
29
+ toSessionKey: string;
30
+ fromAgentId?: string;
31
+ toAgentId: string;
32
+ assignmentEpoch: number;
33
+ payload: Record<string, unknown>;
34
+ createdAt: number;
35
+ }
36
+ export declare class TaskConversationConflictError extends Error {
37
+ }
38
+ export declare class TaskConversationRepository {
39
+ findHandoffByIdempotencyKey(input: {
40
+ taskId: string;
41
+ expectedTaskVersion: number;
42
+ toAgentId: string;
43
+ idempotencyKey: string;
44
+ }): TaskHandoffSnapshot | undefined;
45
+ getState(taskId: string): TaskConversationState | undefined;
46
+ requireState(taskId: string): TaskConversationState;
47
+ listSessions(taskId: string): TaskSessionLink[];
48
+ getActiveSession(taskId: string): TaskSessionLink | undefined;
49
+ resolveActiveExecutionSession(sessionKey: string): TaskSessionLink | undefined;
50
+ activateExecutionSession(input: {
51
+ taskId: string;
52
+ sessionKey: string;
53
+ agentId: string;
54
+ runId?: string;
55
+ now?: number;
56
+ }): TaskConversationState;
57
+ completeHandoff(input: {
58
+ taskId: string;
59
+ expectedTaskVersion: number;
60
+ toSessionKey: string;
61
+ toAgentId: string;
62
+ idempotencyKey: string;
63
+ payload: Record<string, unknown>;
64
+ now?: number;
65
+ }): {
66
+ state: TaskConversationState;
67
+ snapshot: TaskHandoffSnapshot;
68
+ taskVersion: number;
69
+ };
70
+ getLatestHandoff(taskId: string): TaskHandoffSnapshot | undefined;
71
+ }
@@ -0,0 +1,194 @@
1
+ import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ //#region src/tasks/task-conversation-repository.ts
4
+ var TaskConversationConflictError = class extends Error {};
5
+ function stateFromRow(row) {
6
+ return {
7
+ taskId: row.task_id,
8
+ ...row.active_session_key ? { activeSessionKey: row.active_session_key } : {},
9
+ ...row.current_executor_agent_id ? { currentExecutorAgentId: row.current_executor_agent_id } : {},
10
+ assignmentEpoch: row.assignment_epoch,
11
+ status: row.status,
12
+ updatedAt: row.updated_at
13
+ };
14
+ }
15
+ function linkFromRow(row) {
16
+ if (!row.session_key) return void 0;
17
+ return {
18
+ id: row.task_session_id,
19
+ taskId: row.task_id,
20
+ sessionKey: row.session_key,
21
+ role: row.role,
22
+ ...row.agent_id ? { agentId: row.agent_id } : {},
23
+ ...row.run_id ? { runId: row.run_id } : {},
24
+ assignmentEpoch: row.assignment_epoch,
25
+ status: row.status,
26
+ startedAt: row.started_at ?? row.created_at,
27
+ ...row.ended_at === null ? {} : { endedAt: row.ended_at },
28
+ createdAt: row.created_at
29
+ };
30
+ }
31
+ function snapshotFromRow(row) {
32
+ return {
33
+ id: row.snapshot_id,
34
+ taskId: row.task_id,
35
+ ...row.from_session_key ? { fromSessionKey: row.from_session_key } : {},
36
+ toSessionKey: row.to_session_key,
37
+ ...row.from_agent_id ? { fromAgentId: row.from_agent_id } : {},
38
+ toAgentId: row.to_agent_id,
39
+ assignmentEpoch: row.assignment_epoch,
40
+ payload: JSON.parse(row.payload_json),
41
+ createdAt: row.created_at
42
+ };
43
+ }
44
+ function handoffRequestHash(input) {
45
+ return createHash("sha256").update(JSON.stringify(input)).digest("hex");
46
+ }
47
+ var TaskConversationRepository = class {
48
+ findHandoffByIdempotencyKey(input) {
49
+ const row = getSqliteDatabase().prepare(`SELECT request_hash, result_json FROM command_deduplication
50
+ WHERE idempotency_key = ?`).get(input.idempotencyKey);
51
+ if (!row) return void 0;
52
+ const requestHash = handoffRequestHash(input);
53
+ if (row.request_hash !== requestHash) throw new TaskConversationConflictError("Idempotency key was reused with different input");
54
+ const result = JSON.parse(row.result_json);
55
+ const snapshot = getSqliteDatabase().prepare("SELECT * FROM task_handoff_snapshots WHERE snapshot_id = ?").get(result.snapshotId);
56
+ if (!snapshot || snapshot.task_id !== input.taskId) throw new Error(`Task handoff snapshot not found: ${result.snapshotId}`);
57
+ return snapshotFromRow(snapshot);
58
+ }
59
+ getState(taskId) {
60
+ const row = getSqliteDatabase().prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(taskId);
61
+ return row ? stateFromRow(row) : void 0;
62
+ }
63
+ requireState(taskId) {
64
+ const state = this.getState(taskId);
65
+ if (!state) throw new Error(`Task conversation state not found: ${taskId}`);
66
+ return state;
67
+ }
68
+ listSessions(taskId) {
69
+ return getSqliteDatabase().prepare(`SELECT * FROM task_sessions WHERE task_id = ? AND session_key IS NOT NULL
70
+ ORDER BY assignment_epoch DESC, created_at DESC, task_session_id DESC`).all(taskId).flatMap((row) => {
71
+ const link = linkFromRow(row);
72
+ return link ? [link] : [];
73
+ });
74
+ }
75
+ getActiveSession(taskId) {
76
+ const row = getSqliteDatabase().prepare(`SELECT * FROM task_sessions
77
+ WHERE task_id = ? AND role = 'execution' AND status = 'active'
78
+ LIMIT 1`).get(taskId);
79
+ return row ? linkFromRow(row) : void 0;
80
+ }
81
+ resolveActiveExecutionSession(sessionKey) {
82
+ const row = getSqliteDatabase().prepare(`SELECT * FROM task_sessions
83
+ WHERE session_key = ? AND role = 'execution' AND status = 'active'
84
+ LIMIT 1`).get(sessionKey);
85
+ return row ? linkFromRow(row) : void 0;
86
+ }
87
+ activateExecutionSession(input) {
88
+ const now = input.now ?? Date.now();
89
+ return runSqliteWriteTransaction((db) => {
90
+ const existing = db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId);
91
+ const current = db.prepare(`SELECT * FROM task_sessions
92
+ WHERE task_id = ? AND role = 'execution' AND status = 'active'`).get(input.taskId);
93
+ const sameSession = current?.session_key === input.sessionKey;
94
+ const assignmentEpoch = sameSession ? current.assignment_epoch : (existing?.assignment_epoch ?? 0) + 1;
95
+ if (!sameSession) db.prepare(`UPDATE task_sessions SET status = 'superseded', ended_at = ?
96
+ WHERE task_id = ? AND role = 'execution' AND status = 'active'`).run(now, input.taskId);
97
+ db.prepare(`INSERT INTO task_sessions (
98
+ task_session_id, task_id, session_key, role, created_at,
99
+ agent_id, run_id, assignment_epoch, status, started_at, ended_at
100
+ ) VALUES (?, ?, ?, 'execution', ?, ?, ?, ?, 'active', ?, NULL)
101
+ ON CONFLICT(task_id, session_key, role) WHERE session_key IS NOT NULL DO UPDATE SET
102
+ agent_id = excluded.agent_id,
103
+ run_id = COALESCE(excluded.run_id, task_sessions.run_id),
104
+ assignment_epoch = excluded.assignment_epoch,
105
+ status = 'active',
106
+ started_at = excluded.started_at,
107
+ ended_at = NULL`).run(randomUUID(), input.taskId, input.sessionKey, now, input.agentId, input.runId ?? null, assignmentEpoch, sameSession ? current.started_at ?? now : now);
108
+ db.prepare(`INSERT INTO task_conversation_state (
109
+ task_id, active_session_key, current_executor_agent_id,
110
+ assignment_epoch, status, updated_at
111
+ ) VALUES (?, ?, ?, ?, 'active', ?)
112
+ ON CONFLICT(task_id) DO UPDATE SET
113
+ active_session_key = excluded.active_session_key,
114
+ current_executor_agent_id = excluded.current_executor_agent_id,
115
+ assignment_epoch = excluded.assignment_epoch,
116
+ status = 'active',
117
+ updated_at = excluded.updated_at`).run(input.taskId, input.sessionKey, input.agentId, assignmentEpoch, now);
118
+ return stateFromRow(db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId));
119
+ });
120
+ }
121
+ completeHandoff(input) {
122
+ const now = input.now ?? Date.now();
123
+ return runSqliteWriteTransaction((db) => {
124
+ const requestHash = handoffRequestHash({
125
+ taskId: input.taskId,
126
+ expectedTaskVersion: input.expectedTaskVersion,
127
+ toAgentId: input.toAgentId
128
+ });
129
+ const duplicate = db.prepare(`SELECT request_hash, result_json FROM command_deduplication
130
+ WHERE idempotency_key = ?`).get(input.idempotencyKey);
131
+ if (duplicate) {
132
+ if (duplicate.request_hash !== requestHash) throw new TaskConversationConflictError("Idempotency key was reused with different input");
133
+ const result = JSON.parse(duplicate.result_json);
134
+ const snapshotRow = db.prepare("SELECT * FROM task_handoff_snapshots WHERE snapshot_id = ?").get(result.snapshotId);
135
+ const taskRow = db.prepare("SELECT version FROM tasks WHERE task_id = ?").get(input.taskId);
136
+ return {
137
+ state: stateFromRow(db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId)),
138
+ snapshot: snapshotFromRow(snapshotRow),
139
+ taskVersion: taskRow.version
140
+ };
141
+ }
142
+ const existing = db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId);
143
+ if (!existing) throw new Error(`Task conversation state not found: ${input.taskId}`);
144
+ const current = db.prepare(`SELECT * FROM task_sessions
145
+ WHERE task_id = ? AND role = 'execution' AND status = 'active'`).get(input.taskId);
146
+ if (current?.session_key === input.toSessionKey && current.agent_id === input.toAgentId) throw new TaskConversationConflictError("Executor is already active");
147
+ const assignmentEpoch = existing.assignment_epoch + 1;
148
+ if (db.prepare(`UPDATE tasks SET delegate_agent_id = ?, version = version + 1, updated_at = ?
149
+ WHERE task_id = ? AND version = ?`).run(input.toAgentId, now, input.taskId, input.expectedTaskVersion).changes === 0) throw new TaskConversationConflictError("Task changed");
150
+ db.prepare(`UPDATE task_sessions SET status = 'superseded', ended_at = ?
151
+ WHERE task_id = ? AND role = 'execution' AND status = 'active'`).run(now, input.taskId);
152
+ db.prepare(`INSERT INTO task_sessions (
153
+ task_session_id, task_id, session_key, role, created_at,
154
+ agent_id, assignment_epoch, status, started_at, ended_at
155
+ ) VALUES (?, ?, ?, 'execution', ?, ?, ?, 'active', ?, NULL)`).run(randomUUID(), input.taskId, input.toSessionKey, now, input.toAgentId, assignmentEpoch, now);
156
+ const snapshotId = randomUUID();
157
+ db.prepare(`INSERT INTO task_handoff_snapshots (
158
+ snapshot_id, task_id, from_session_key, to_session_key,
159
+ from_agent_id, to_agent_id, assignment_epoch, payload_json, created_at
160
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(snapshotId, input.taskId, current?.session_key ?? null, input.toSessionKey, current?.agent_id ?? existing.current_executor_agent_id ?? null, input.toAgentId, assignmentEpoch, JSON.stringify(input.payload), now);
161
+ db.prepare(`UPDATE task_conversation_state SET
162
+ active_session_key = ?, current_executor_agent_id = ?,
163
+ assignment_epoch = ?, status = 'active', updated_at = ?
164
+ WHERE task_id = ?`).run(input.toSessionKey, input.toAgentId, assignmentEpoch, now, input.taskId);
165
+ db.prepare(`INSERT INTO command_deduplication (
166
+ idempotency_key, command_type, subject_kind, subject_id,
167
+ request_hash, result_json, created_at
168
+ ) VALUES (?, 'task.handoff', 'task', ?, ?, ?, ?)`).run(input.idempotencyKey, input.taskId, requestHash, JSON.stringify({ snapshotId }), now);
169
+ return {
170
+ state: stateFromRow(db.prepare("SELECT * FROM task_conversation_state WHERE task_id = ?").get(input.taskId)),
171
+ taskVersion: input.expectedTaskVersion + 1,
172
+ snapshot: {
173
+ id: snapshotId,
174
+ taskId: input.taskId,
175
+ ...current?.session_key ? { fromSessionKey: current.session_key } : {},
176
+ toSessionKey: input.toSessionKey,
177
+ ...current?.agent_id || existing.current_executor_agent_id ? { fromAgentId: current?.agent_id ?? existing.current_executor_agent_id } : {},
178
+ toAgentId: input.toAgentId,
179
+ assignmentEpoch,
180
+ payload: input.payload,
181
+ createdAt: now
182
+ }
183
+ };
184
+ });
185
+ }
186
+ getLatestHandoff(taskId) {
187
+ const row = getSqliteDatabase().prepare(`SELECT * FROM task_handoff_snapshots
188
+ WHERE task_id = ? ORDER BY assignment_epoch DESC LIMIT 1`).get(taskId);
189
+ if (!row) return void 0;
190
+ return snapshotFromRow(row);
191
+ }
192
+ };
193
+ //#endregion
194
+ export { TaskConversationConflictError, TaskConversationRepository };
@@ -0,0 +1,31 @@
1
+ import type { Config } from '../config/schema.js';
2
+ import type { SessionIndex } from '../session/index.js';
3
+ import { type TaskConversationState, type TaskHandoffSnapshot } from './task-conversation-repository.js';
4
+ import { type TaskAggregate } from './task-repository.js';
5
+ export interface TaskHandoffResult {
6
+ task: TaskAggregate;
7
+ conversation: TaskConversationState;
8
+ snapshot: TaskHandoffSnapshot;
9
+ fromAgentId?: string;
10
+ toAgentId: string;
11
+ activeSessionKey: string;
12
+ assignmentEpoch: number;
13
+ }
14
+ export declare class TaskHandoffService {
15
+ #private;
16
+ private readonly deps;
17
+ constructor(deps: {
18
+ getConfig: () => Config;
19
+ sessionIndex: SessionIndex;
20
+ getActiveRunId: (sessionKey: string) => string | undefined;
21
+ abortRun: (runId: string) => Promise<unknown>;
22
+ });
23
+ handoff(input: {
24
+ taskId: string;
25
+ toAgentId: string;
26
+ expectedVersion: number;
27
+ idempotencyKey: string;
28
+ }): Promise<TaskHandoffResult>;
29
+ private performHandoff;
30
+ private abortPreviousSession;
31
+ }