@the-open-engine/zeroshot 6.23.0 → 6.25.0

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 (51) hide show
  1. package/cli/index.js +37 -2
  2. package/docker/zeroshot-cluster/Dockerfile +7 -0
  3. package/lib/agent-cli-provider/adapters/omp.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/omp.js +37 -11
  5. package/lib/agent-cli-provider/adapters/omp.js.map +1 -1
  6. package/lib/agent-cli-provider/omp-release.d.ts +3 -0
  7. package/lib/agent-cli-provider/omp-release.d.ts.map +1 -1
  8. package/lib/agent-cli-provider/omp-release.js +20 -1
  9. package/lib/agent-cli-provider/omp-release.js.map +1 -1
  10. package/lib/agent-cli-provider/omp-rpc-driver.d.ts.map +1 -1
  11. package/lib/agent-cli-provider/omp-rpc-driver.js +27 -2
  12. package/lib/agent-cli-provider/omp-rpc-driver.js.map +1 -1
  13. package/lib/agent-cli-provider/omp-rpc-session.js +3 -3
  14. package/lib/agent-cli-provider/omp-rpc-session.js.map +1 -1
  15. package/lib/agent-cli-provider/provider-registry.d.ts +20 -8
  16. package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
  17. package/lib/agent-cli-provider/provider-registry.js +56 -9
  18. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  19. package/lib/agent-cli-provider/types.d.ts +2 -0
  20. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  21. package/lib/agent-cli-provider/types.js.map +1 -1
  22. package/lib/docker-config.js +122 -5
  23. package/package.json +2 -2
  24. package/src/agent/agent-lifecycle.js +78 -3
  25. package/src/agent/agent-task-executor.js +72 -3
  26. package/src/agent/provider-session.js +112 -2
  27. package/src/agent-cli-provider/adapters/omp.ts +41 -11
  28. package/src/agent-cli-provider/omp-release.ts +26 -0
  29. package/src/agent-cli-provider/omp-rpc-driver.ts +31 -3
  30. package/src/agent-cli-provider/omp-rpc-session.ts +3 -3
  31. package/src/agent-cli-provider/provider-registry.ts +102 -20
  32. package/src/agent-cli-provider/types.ts +4 -0
  33. package/src/isolation-manager.js +535 -89
  34. package/src/omp-blob-root.js +110 -0
  35. package/src/omp-config-overlay.js +9 -1
  36. package/src/omp-execution-fingerprint.js +62 -0
  37. package/src/omp-session-limits.js +17 -0
  38. package/src/omp-session-partition.js +297 -0
  39. package/src/omp-session-verifier.js +576 -0
  40. package/src/orchestrator.js +11 -1
  41. package/src/preflight.js +15 -2
  42. package/task-lib/commands/clean.js +23 -0
  43. package/task-lib/commands/resume.js +42 -0
  44. package/task-lib/commands/run.js +65 -0
  45. package/task-lib/omp-session-cleanup.js +160 -0
  46. package/task-lib/omp-session-ownership-schema.js +262 -0
  47. package/task-lib/omp-session-ownership.js +332 -0
  48. package/task-lib/omp-storage-root.js +35 -0
  49. package/task-lib/rpc-watcher.js +332 -2
  50. package/task-lib/runner.js +195 -4
  51. package/task-lib/store.js +42 -7
@@ -1,16 +1,25 @@
1
1
  import { fork } from 'child_process';
2
- import { join, dirname } from 'path';
2
+ import { join, dirname, resolve as resolvePath } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { mkdirSync } from 'fs';
4
5
  import { LOGS_DIR } from './config.js';
5
6
  import { addTask, generateId, ensureDirs } from './store.js';
7
+ import {
8
+ isOmpSessionlessRun,
9
+ resolveOmpStorageRoot,
10
+ resolveOmpOwnerKind,
11
+ } from './omp-storage-root.js';
12
+ import { readOwnership, writeProvisionalOwnership } from './omp-session-ownership.js';
6
13
  import { createRequire } from 'module';
7
14
 
8
15
  const require = createRequire(import.meta.url);
9
16
  const {
10
17
  buildOmpPrompt,
11
18
  getProviderRegistryEntry,
19
+ normalizeProviderName,
12
20
  prepareSingleAgentProviderCommand,
13
21
  } = require('./provider-helper-runtime.js');
22
+ const { getDefaultProviderId } = require('../lib/provider-names.js');
14
23
  const {
15
24
  ISOLATED_SETTINGS_FILE_ENV,
16
25
  ISOLATED_SETTINGS_FILE_MARKER,
@@ -23,6 +32,11 @@ const {
23
32
  } = require('../src/worktree-claude-config');
24
33
  const { TASK_SPAWN_OWNERSHIP_TOKEN_ENV } = require('../src/task-spawn-cleanup-ownership');
25
34
  const { sendWatcherPrompt } = require('../src/watcher-prompt-channel');
35
+ const {
36
+ generateOmpPartitionId,
37
+ partitionPathFor,
38
+ createOmpSessionPartitionDirectory,
39
+ } = require('../src/omp-session-partition');
26
40
  export {
27
41
  isOwnedProcessTreeRunning,
28
42
  isProcessRunning,
@@ -32,6 +46,162 @@ export {
32
46
 
33
47
  const __dirname = dirname(fileURLToPath(import.meta.url));
34
48
 
49
+ /**
50
+ * Cross-check the caller-supplied resume descriptor against the prior owner's *persisted* record
51
+ * and return the expectation the watcher will re-verify.
52
+ *
53
+ * The descriptor arrives over argv (from the agent's own `providerSession.ompSession` snapshot, or
54
+ * from `zeroshot task resume`), so it is never trusted on its own: the task row named by
55
+ * `priorOwnerTaskId` is the authority, and every field the descriptor asserts must match it
56
+ * exactly. A descriptor and a row that disagree are conflicting identities and fail closed here,
57
+ * before a task row is even created — the "conflicting IDs never reach a resume prompt" case.
58
+ */
59
+ export function resolveOmpResumeExpectation({ descriptor, storageRoot, canonicalWorkspace }) {
60
+ const prior = readOwnership(descriptor.priorOwnerTaskId);
61
+ if (!prior) {
62
+ throw new Error(
63
+ `OMP resume: task ${descriptor.priorOwnerTaskId} has no valid OMP session ownership record.`
64
+ );
65
+ }
66
+ if (prior.state !== 'committed' || !prior.session || !prior.partitionIdentity) {
67
+ throw new Error(
68
+ `OMP resume: task ${descriptor.priorOwnerTaskId} ownership is '${prior.state}', not a committed resumable session.`
69
+ );
70
+ }
71
+ if (prior.storageRoot !== resolvePath(storageRoot)) {
72
+ throw new Error(
73
+ `OMP resume: storage root ${resolvePath(storageRoot)} does not match the recorded ${prior.storageRoot}.`
74
+ );
75
+ }
76
+ // Moved/deleted workspace and "existing-but-wrong recorded cwd": a session belongs to the
77
+ // workspace it was recorded against and may never be continued from a different one.
78
+ if (prior.canonicalWorkspace !== resolvePath(canonicalWorkspace)) {
79
+ throw new Error(
80
+ `OMP resume: workspace ${resolvePath(canonicalWorkspace)} does not match the recorded ${prior.canonicalWorkspace}.`
81
+ );
82
+ }
83
+
84
+ const mismatches = [];
85
+ const requireExact = (label, actual, expected) => {
86
+ if (actual !== expected) mismatches.push(`${label} (${actual} != ${expected})`);
87
+ };
88
+ requireExact('partitionId', descriptor.partitionId, prior.partitionId);
89
+ requireExact('sessionId', descriptor.expectedSessionId, prior.session.sessionId);
90
+ requireExact('sessionFileName', descriptor.sessionFileName, prior.session.fileName);
91
+ requireExact(
92
+ 'sessionFileIdentity',
93
+ `${descriptor.expectedSessionFileIdentity?.device}:${descriptor.expectedSessionFileIdentity?.inode}`,
94
+ `${prior.session.fileIdentity.device}:${prior.session.fileIdentity.inode}`
95
+ );
96
+ // partitionIdentity is deliberately absent from the agent's `providerSession.ompSession`
97
+ // snapshot (issue #866 fixes that field list, and the snapshot never carries partition paths or
98
+ // storage-root state), so it is authoritative from the row only. `zeroshot task resume`, which
99
+ // reads the row directly, does assert it — check it whenever it is supplied.
100
+ if (descriptor.expectedPartitionIdentity !== undefined) {
101
+ requireExact(
102
+ 'partitionIdentity',
103
+ `${descriptor.expectedPartitionIdentity?.device}:${descriptor.expectedPartitionIdentity?.inode}`,
104
+ `${prior.partitionIdentity.device}:${prior.partitionIdentity.inode}`
105
+ );
106
+ }
107
+ requireExact(
108
+ 'artifactManifestDigest',
109
+ descriptor.expectedArtifactManifestDigest,
110
+ prior.session.artifactManifestDigest
111
+ );
112
+ requireExact(
113
+ 'executionFingerprint',
114
+ descriptor.expectedExecutionFingerprint,
115
+ prior.session.executionFingerprint
116
+ );
117
+ requireExact(
118
+ 'selectedProvider',
119
+ descriptor.expectedSelectedProvider,
120
+ prior.session.selectedProvider
121
+ );
122
+ requireExact('selectedModel', descriptor.expectedSelectedModel, prior.session.selectedModel);
123
+ if (mismatches.length > 0) {
124
+ throw new Error(
125
+ `OMP resume: descriptor conflicts with the persisted owner record: ${mismatches.join(', ')}.`
126
+ );
127
+ }
128
+
129
+ return {
130
+ priorOwnerTaskId: descriptor.priorOwnerTaskId,
131
+ partitionId: prior.partitionId,
132
+ partitionPath: prior.partitionPath,
133
+ canonicalWorkspace: prior.canonicalWorkspace,
134
+ sessionFileName: prior.session.fileName,
135
+ sessionFilePath: join(prior.partitionPath, prior.session.fileName),
136
+ expectedSessionId: prior.session.sessionId,
137
+ expectedPartitionIdentity: prior.partitionIdentity,
138
+ expectedSessionFileIdentity: prior.session.fileIdentity,
139
+ expectedArtifactManifestDigest: prior.session.artifactManifestDigest,
140
+ expectedExecutionFingerprint: prior.session.executionFingerprint,
141
+ expectedSelectedProvider: prior.session.selectedProvider,
142
+ expectedSelectedModel: prior.session.selectedModel,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Resolve this task's OMP session plan, or null for every other provider / structured-output
148
+ * recovery turn. Allocates (but does not yet create on disk) a fresh partition, or resolves the
149
+ * resume descriptor against the prior owner's persisted record. The partition directory itself is
150
+ * created only after the task row is durable (see spawnTask below — row-before-directory), and
151
+ * the structural/identity/fingerprint verification plus the owner transfer happen in the rpc-stdio
152
+ * watcher, not here.
153
+ */
154
+ function resolveOmpSessionPlan({ id, cwd, options }) {
155
+ if (options.structuredOutputRecovery) return null;
156
+ const providerName = normalizeProviderName(options.provider || getDefaultProviderId());
157
+ if (providerName !== 'omp') return null;
158
+ // Docker stays fresh-only (issue #866). Returning null here means no partition is allocated, no
159
+ // ownership row is written, and the adapter falls back to `--no-session`.
160
+ if (isOmpSessionlessRun(options)) return null;
161
+
162
+ const storageRoot = resolveOmpStorageRoot(options);
163
+ mkdirSync(storageRoot, { recursive: true });
164
+ const ownerKind = resolveOmpOwnerKind(options);
165
+ const owner = { ...ownerKind, taskId: id };
166
+
167
+ if (options.ompResume) {
168
+ const expectation = resolveOmpResumeExpectation({
169
+ descriptor: options.ompResume,
170
+ storageRoot,
171
+ canonicalWorkspace: cwd,
172
+ });
173
+ return {
174
+ session: {
175
+ kind: 'resume',
176
+ partition: { path: expectation.partitionPath },
177
+ file: { path: expectation.sessionFilePath },
178
+ },
179
+ resumeExpectation: expectation,
180
+ provisionalOwnership: writeProvisionalOwnership({
181
+ partitionId: expectation.partitionId,
182
+ storageRoot,
183
+ canonicalWorkspace: cwd,
184
+ owner,
185
+ }),
186
+ createDirectory: () => {}, // must already exist; the watcher verifies before spawn
187
+ };
188
+ }
189
+
190
+ const partitionId = generateOmpPartitionId();
191
+ const partitionPath = partitionPathFor(storageRoot, partitionId);
192
+ return {
193
+ session: { kind: 'fresh', partition: { path: partitionPath } },
194
+ resumeExpectation: null,
195
+ provisionalOwnership: writeProvisionalOwnership({
196
+ partitionId,
197
+ storageRoot,
198
+ canonicalWorkspace: cwd,
199
+ owner,
200
+ }),
201
+ createDirectory: () => createOmpSessionPartitionDirectory(partitionPath),
202
+ };
203
+ }
204
+
35
205
  export function spawnTask(prompt, options = {}) {
36
206
  ensureDirs();
37
207
 
@@ -41,10 +211,12 @@ export function spawnTask(prompt, options = {}) {
41
211
 
42
212
  const outputFormat = resolveOutputFormat(options);
43
213
  const jsonSchema = resolveJsonSchema(options, outputFormat);
214
+ const ompPlan = resolveOmpSessionPlan({ id, cwd, options });
44
215
  const prepared = prepareTaskProviderCommandFromResolved(prompt, options, {
45
216
  outputFormat,
46
217
  jsonSchema,
47
218
  cwd,
219
+ ompSession: ompPlan?.session,
48
220
  });
49
221
  const providerName = prepared.adapter.id;
50
222
  const modelSpec = prepared.options.modelSpec;
@@ -59,16 +231,23 @@ export function spawnTask(prompt, options = {}) {
59
231
  providerName,
60
232
  modelSpec,
61
233
  commandSpec,
234
+ ompSessionOwnership: ompPlan?.provisionalOwnership ?? null,
62
235
  });
63
236
 
237
+ // Row-before-directory: the SQL row is durable proof of an attempted allocation before the
238
+ // partition directory (or anything else OMP-owned) exists on disk. A crash between these two
239
+ // lines leaves a provisional row pointing at a path with nothing there yet — cleanup safely
240
+ // no-ops on a nonexistent path, and normal task-lifecycle recovery handles the row itself.
64
241
  addTask(task);
242
+ ompPlan?.createDirectory();
65
243
 
66
244
  const watcherConfig = buildWatcherConfig(
67
245
  outputFormat,
68
246
  jsonSchema,
69
247
  options,
70
248
  providerName,
71
- commandSpec
249
+ commandSpec,
250
+ ompPlan
72
251
  );
73
252
  const watcherScript = resolveWatcherScript(
74
253
  {
@@ -137,6 +316,7 @@ function buildProviderOptions(options, runtime, modelSelection) {
137
316
  ...(structuredOutputRecovery ? {} : mcpConfigOption(options)),
138
317
  ...claudeSettingsFileOption(),
139
318
  ...(!structuredOutputRecovery && options.resume ? { resumeSessionId: options.resume } : {}),
319
+ ...(runtime.ompSession ? { ompSession: runtime.ompSession } : {}),
140
320
  ...(process.env.ZEROSHOT_OPENCODE_AGENT?.trim()
141
321
  ? { agentName: process.env.ZEROSHOT_OPENCODE_AGENT.trim() }
142
322
  : {}),
@@ -218,6 +398,7 @@ export function buildTaskRecord({
218
398
  providerName,
219
399
  modelSpec,
220
400
  commandSpec = {},
401
+ ompSessionOwnership = null,
221
402
  }) {
222
403
  return {
223
404
  id,
@@ -251,6 +432,7 @@ export function buildTaskRecord({
251
432
  terminationStrategy: null,
252
433
  cancelRequested: false,
253
434
  spawnOwnershipToken: process.env[TASK_SPAWN_OWNERSHIP_TOKEN_ENV] || null,
435
+ ompSessionOwnership,
254
436
  commandCleanup:
255
437
  commandSpec.cleanup?.length > 0
256
438
  ? {
@@ -267,8 +449,8 @@ function isRpcStdioLane(providerName) {
267
449
 
268
450
  // The returned object is JSON-serialized into the detached watcher's argv, so it must never carry
269
451
  // prompt or other task content: `ps` and /proc/<pid>/cmdline expose argv to every local user for
270
- // the whole lifetime of the watcher.
271
- function buildWatcherConfig(outputFormat, jsonSchema, options, providerName, commandSpec) {
452
+ // the whole lifetime of the watcher. Partition paths, ids, and digests are not secret.
453
+ function buildWatcherConfig(outputFormat, jsonSchema, options, providerName, commandSpec, ompPlan) {
272
454
  return {
273
455
  outputFormat,
274
456
  jsonSchema,
@@ -278,6 +460,15 @@ function buildWatcherConfig(outputFormat, jsonSchema, options, providerName, com
278
460
  command: commandSpec.binary,
279
461
  env: commandSpec.env || {},
280
462
  commandSpec: buildWatcherCommandSpec(commandSpec, isRpcStdioLane(providerName)),
463
+ ...(ompPlan
464
+ ? {
465
+ ompSession: ompPlan.session,
466
+ ompResumeExpectation: ompPlan.resumeExpectation,
467
+ // The workspace the ownership row was canonicalized against; the watcher compares it to
468
+ // the session header's own recorded cwd after materialization.
469
+ ompCanonicalWorkspace: ompPlan.provisionalOwnership.canonicalWorkspace,
470
+ }
471
+ : {}),
281
472
  };
282
473
  }
283
474
 
package/task-lib/store.js CHANGED
@@ -10,9 +10,13 @@ import { join } from 'path';
10
10
  import Database from 'better-sqlite3';
11
11
  import { TASKS_DIR, LOGS_DIR } from './config.js';
12
12
  import { generateName } from './name-generator.js';
13
+ import {
14
+ parseOmpSessionOwnership,
15
+ serializeOmpSessionOwnership,
16
+ } from './omp-session-ownership-schema.js';
13
17
 
14
18
  const DB_FILE = join(TASKS_DIR, 'store.db');
15
- export const TASK_STORE_SCHEMA_VERSION = 4;
19
+ export const TASK_STORE_SCHEMA_VERSION = 5;
16
20
 
17
21
  /** @type {Database.Database | null} */
18
22
  let db = null;
@@ -59,7 +63,8 @@ function getDb() {
59
63
  termination_strategy TEXT,
60
64
  command_cleanup TEXT,
61
65
  cancel_requested INTEGER DEFAULT 0,
62
- spawn_ownership_token TEXT
66
+ spawn_ownership_token TEXT,
67
+ omp_session_ownership TEXT
63
68
  );
64
69
 
65
70
  CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
@@ -110,6 +115,15 @@ function serializeCommandCleanup(value) {
110
115
  return value ? JSON.stringify(value) : null;
111
116
  }
112
117
 
118
+ /**
119
+ * Internal accessor for modules that need direct prepared-statement access (SQL compare-and-swap
120
+ * transitions) beyond what the generic load/save/update helpers below offer. See
121
+ * task-lib/omp-session-ownership.js.
122
+ */
123
+ export function getTaskStoreDatabase() {
124
+ return getDb();
125
+ }
126
+
113
127
  export function migrateTaskStore(database) {
114
128
  ensureTaskColumn(database, 'process_group_id', 'INTEGER');
115
129
  ensureTaskColumn(database, 'termination_strategy', 'TEXT');
@@ -119,6 +133,9 @@ export function migrateTaskStore(database) {
119
133
  ensureTaskColumn(database, 'requested_resume_session_id', 'TEXT');
120
134
  ensureTaskColumn(database, 'session_id_conflict', 'INTEGER NOT NULL DEFAULT 0');
121
135
  ensureTaskColumn(database, 'resume_identity_verified', 'INTEGER NOT NULL DEFAULT 0');
136
+ // No backfill: every pre-v5 row has no OMP session concept, so NULL is exact truth, never a
137
+ // fabricated value. A non-OMP task's resume path is untouched by this column.
138
+ ensureTaskColumn(database, 'omp_session_ownership', 'TEXT');
122
139
  database.exec(`
123
140
  CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_spawn_ownership_token
124
141
  ON tasks(spawn_ownership_token)
@@ -144,6 +161,9 @@ export function migrateTaskStore(database) {
144
161
  if (version < 4) {
145
162
  database.prepare('UPDATE tasks SET resume_identity_verified = 0').run();
146
163
  }
164
+ if (version < 5) {
165
+ // omp_session_ownership already defaults to NULL via ensureTaskColumn above; no backfill.
166
+ }
147
167
  database.pragma(`user_version = ${TASK_STORE_SCHEMA_VERSION}`);
148
168
  })();
149
169
  }
@@ -192,6 +212,7 @@ function rowToTask(row) {
192
212
  commandCleanup: parseCommandCleanup(row.command_cleanup),
193
213
  cancelRequested: Boolean(row.cancel_requested),
194
214
  spawnOwnershipToken: row.spawn_ownership_token,
215
+ ompSessionOwnership: parseOmpSessionOwnership(row.omp_session_ownership),
195
216
  };
196
217
  }
197
218
 
@@ -220,12 +241,12 @@ export function saveTasks(tasks) {
220
241
  id, prompt, full_prompt, cwd, status, pid, session_id, session_id_conflict, requested_resume_session_id, resume_identity_verified, log_file,
221
242
  created_at, updated_at, exit_code, error, provider, model,
222
243
  schedule_id, socket_path, attachable, process_group_id, termination_strategy,
223
- command_cleanup, cancel_requested, spawn_ownership_token
244
+ command_cleanup, cancel_requested, spawn_ownership_token, omp_session_ownership
224
245
  ) VALUES (
225
246
  @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @sessionIdConflict, @requestedResumeSessionId, @resumeIdentityVerified, @logFile,
226
247
  @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
227
248
  @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy,
228
- @commandCleanup, @cancelRequested, @spawnOwnershipToken
249
+ @commandCleanup, @cancelRequested, @spawnOwnershipToken, @ompSessionOwnership
229
250
  )
230
251
  `);
231
252
 
@@ -260,6 +281,7 @@ export function saveTasks(tasks) {
260
281
  commandCleanup: serializeCommandCleanup(task.commandCleanup),
261
282
  cancelRequested: task.cancelRequested ? 1 : 0,
262
283
  spawnOwnershipToken: task.spawnOwnershipToken || null,
284
+ ompSessionOwnership: serializeOmpSessionOwnership(task.ompSessionOwnership || null),
263
285
  });
264
286
  }
265
287
  });
@@ -350,7 +372,15 @@ export function updateTask(id, updates) {
350
372
  termination_strategy = @terminationStrategy,
351
373
  command_cleanup = @commandCleanup,
352
374
  cancel_requested =
353
- CASE WHEN @hasCancelRequested = 1 THEN @cancelRequested ELSE cancel_requested END
375
+ CASE WHEN @hasCancelRequested = 1 THEN @cancelRequested ELSE cancel_requested END,
376
+ -- Only ever written when the caller explicitly supplies it. This is a read-modify-write
377
+ -- update, so unconditionally rewriting the ownership column would let an unrelated
378
+ -- updateTask (the watcher persisting spawn evidence, say) clobber an owner-fenced
379
+ -- compare-and-swap another process performed in between — see
380
+ -- task-lib/omp-session-ownership.js, whose transitions bypass this statement for exactly
381
+ -- that reason.
382
+ omp_session_ownership =
383
+ CASE WHEN @hasOmpSessionOwnership = 1 THEN @ompSessionOwnership ELSE omp_session_ownership END
354
384
  WHERE id = @id
355
385
  `
356
386
  )
@@ -379,6 +409,10 @@ export function updateTask(id, updates) {
379
409
  commandCleanup: serializeCommandCleanup(updated.commandCleanup),
380
410
  hasCancelRequested: Object.prototype.hasOwnProperty.call(updates, 'cancelRequested') ? 1 : 0,
381
411
  cancelRequested: updated.cancelRequested ? 1 : 0,
412
+ hasOmpSessionOwnership: Object.prototype.hasOwnProperty.call(updates, 'ompSessionOwnership')
413
+ ? 1
414
+ : 0,
415
+ ompSessionOwnership: serializeOmpSessionOwnership(updated.ompSessionOwnership || null),
382
416
  });
383
417
 
384
418
  return updated;
@@ -404,12 +438,12 @@ export function addTask(task) {
404
438
  id, prompt, full_prompt, cwd, status, pid, session_id, session_id_conflict, requested_resume_session_id, resume_identity_verified, log_file,
405
439
  created_at, updated_at, exit_code, error, provider, model,
406
440
  schedule_id, socket_path, attachable, process_group_id, termination_strategy,
407
- command_cleanup, cancel_requested, spawn_ownership_token
441
+ command_cleanup, cancel_requested, spawn_ownership_token, omp_session_ownership
408
442
  ) VALUES (
409
443
  @id, @prompt, @fullPrompt, @cwd, @status, @pid, @sessionId, @sessionIdConflict, @requestedResumeSessionId, @resumeIdentityVerified, @logFile,
410
444
  @createdAt, @updatedAt, @exitCode, @error, @provider, @model,
411
445
  @scheduleId, @socketPath, @attachable, @processGroupId, @terminationStrategy,
412
- @commandCleanup, @cancelRequested, @spawnOwnershipToken
446
+ @commandCleanup, @cancelRequested, @spawnOwnershipToken, @ompSessionOwnership
413
447
  )
414
448
  `
415
449
  )
@@ -439,6 +473,7 @@ export function addTask(task) {
439
473
  commandCleanup: serializeCommandCleanup(fullTask.commandCleanup),
440
474
  cancelRequested: fullTask.cancelRequested ? 1 : 0,
441
475
  spawnOwnershipToken: fullTask.spawnOwnershipToken || null,
476
+ ompSessionOwnership: serializeOmpSessionOwnership(fullTask.ompSessionOwnership || null),
442
477
  });
443
478
 
444
479
  return fullTask;