@agent-native/core 0.171.3 → 0.172.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 (55) hide show
  1. package/dist/agent/actions/set-tool-approval-policy.d.ts +8 -0
  2. package/dist/agent/actions/set-tool-approval-policy.js +26 -0
  3. package/dist/agent/harness/migrations.js +8 -0
  4. package/dist/agent/harness/runner.js +114 -33
  5. package/dist/agent/harness/store.d.ts +9 -0
  6. package/dist/agent/harness/store.js +87 -23
  7. package/dist/agent/production-agent.d.ts +3 -0
  8. package/dist/agent/production-agent.js +23 -1
  9. package/dist/agent/run-manager.js +40 -9
  10. package/dist/agent/tool-approval-migrations.d.ts +21 -0
  11. package/dist/agent/tool-approval-migrations.js +32 -0
  12. package/dist/agent/tool-approval-store.d.ts +11 -0
  13. package/dist/agent/tool-approval-store.js +76 -1
  14. package/dist/cli/code-agent-executor.js +20 -3
  15. package/dist/client/AgentPanel.d.ts +4 -1
  16. package/dist/client/AgentPanel.js +11 -3
  17. package/dist/client/AssistantChat.d.ts +5 -5
  18. package/dist/client/AssistantChat.js +24 -11
  19. package/dist/client/chat/tool-call-display.d.ts +5 -5
  20. package/dist/client/chat/tool-call-display.js +27 -11
  21. package/dist/client/resources/McpIntegrationDialog.js +5 -5
  22. package/dist/client/resources/mcp-integration-catalog.js +1 -1
  23. package/dist/coding-tools/index.d.ts +31 -0
  24. package/dist/coding-tools/index.js +139 -3
  25. package/dist/collab/struct-routes.d.ts +1 -1
  26. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  27. package/dist/localization/core-messages/ar-SA.js +3 -0
  28. package/dist/localization/core-messages/de-DE.js +3 -0
  29. package/dist/localization/core-messages/en-US.d.ts +3 -0
  30. package/dist/localization/core-messages/en-US.js +3 -0
  31. package/dist/localization/core-messages/es-ES.js +3 -0
  32. package/dist/localization/core-messages/fr-FR.js +3 -0
  33. package/dist/localization/core-messages/hi-IN.js +3 -0
  34. package/dist/localization/core-messages/ja-JP.js +3 -0
  35. package/dist/localization/core-messages/ko-KR.js +3 -0
  36. package/dist/localization/core-messages/pt-BR.js +3 -0
  37. package/dist/localization/core-messages/zh-CN.js +3 -0
  38. package/dist/localization/core-messages/zh-TW.js +3 -0
  39. package/dist/localization/core-messages.d.ts +3 -0
  40. package/dist/localization/default-messages.d.ts +1 -0
  41. package/dist/localization/default-messages.js +7 -6
  42. package/dist/progress/routes.d.ts +1 -1
  43. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  44. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  45. package/dist/resources/handlers.d.ts +1 -1
  46. package/dist/secrets/routes.d.ts +6 -6
  47. package/dist/server/action-discovery.js +5 -0
  48. package/dist/server/agent-chat/prompt-resources.d.ts +9 -0
  49. package/dist/server/agent-chat/prompt-resources.js +12 -2
  50. package/dist/server/realtime-token.d.ts +1 -1
  51. package/dist/server/release-schema.js +4 -1
  52. package/dist/triggers/dispatcher.d.ts +24 -0
  53. package/dist/triggers/dispatcher.js +76 -18
  54. package/dist/vite/action-types-plugin.js +4 -0
  55. package/package.json +2 -1
@@ -0,0 +1,8 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ toolName: string;
3
+ enabled?: boolean;
4
+ }, {
5
+ toolName: string;
6
+ enabled: boolean;
7
+ }>;
8
+ export default _default;
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { defineAction } from "../../action.js";
3
+ import { setAgentToolApprovalPolicy } from "../tool-approval-store.js";
4
+ export default defineAction({
5
+ description: "Enable or disable future approval prompts for one action type for the signed-in user.",
6
+ schema: z.object({
7
+ toolName: z.string().trim().min(1).max(128),
8
+ enabled: z.boolean().default(true),
9
+ }),
10
+ http: { method: "POST" },
11
+ agentTool: false,
12
+ run: async (args, context) => {
13
+ const ownerEmail = context?.userEmail?.trim().toLowerCase();
14
+ if (!ownerEmail)
15
+ throw new Error("Sign in to manage approval preferences.");
16
+ await setAgentToolApprovalPolicy({
17
+ binding: {
18
+ ownerEmail,
19
+ orgId: context?.orgId?.trim() || null,
20
+ toolName: args.toolName,
21
+ },
22
+ enabled: args.enabled,
23
+ });
24
+ return { toolName: args.toolName, enabled: args.enabled };
25
+ },
26
+ });
@@ -57,4 +57,12 @@ export const AGENT_HARNESS_SESSION_MIGRATIONS = [
57
57
  sqlite: "",
58
58
  },
59
59
  },
60
+ {
61
+ version: 4,
62
+ name: "agent-harness-sessions-generation",
63
+ sql: {
64
+ postgres: "ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS generation BIGINT NOT NULL DEFAULT 0",
65
+ sqlite: "ALTER TABLE agent_harness_sessions ADD COLUMN generation INTEGER NOT NULL DEFAULT 0",
66
+ },
67
+ },
60
68
  ];
@@ -1,12 +1,19 @@
1
1
  import { startRun } from "../run-manager.js";
2
2
  import { registerLiveAgentHarnessSession, releaseLiveAgentHarnessSession, } from "./lifecycle.js";
3
- import { getAgentHarnessSession, markAgentHarnessSessionStopped, saveAgentHarnessSession, updateAgentHarnessSession, } from "./store.js";
3
+ import { getAgentHarnessSession, isAgentHarnessSessionConflictError, markAgentHarnessSessionStopped, saveAgentHarnessSession, updateAgentHarnessSession, } from "./store.js";
4
4
  import { agentHarnessEventToAgentChatEvents } from "./translate.js";
5
5
  export function startAgentHarnessRun(opts) {
6
6
  let harnessSession = opts.session;
7
7
  const detachOnComplete = opts.detachOnComplete !== false;
8
- return startRun(opts.runId, opts.threadId, async (send, signal) => {
8
+ return startRun(opts.runId, opts.threadId, async (send, signal, control) => {
9
+ const runControl = control ?? {
10
+ turnSignal: signal,
11
+ chunkSignal: signal,
12
+ chunkBoundaryReason: () => null,
13
+ beginChunk: () => signal,
14
+ };
9
15
  let storedSessionId;
16
+ let harnessSessionRegistered = false;
10
17
  let keepLiveSession = false;
11
18
  try {
12
19
  send({
@@ -20,7 +27,7 @@ export function startAgentHarnessRun(opts) {
20
27
  runId: opts.runId,
21
28
  ownerEmail: opts.ownerEmail ?? opts.createSession?.ownerEmail ?? null,
22
29
  orgId: opts.orgId ?? opts.createSession?.orgId ?? null,
23
- signal,
30
+ signal: runControl.turnSignal,
24
31
  });
25
32
  storedSessionId = opts.createSession?.sessionId ?? harnessSession.id;
26
33
  await saveAgentHarnessSession({
@@ -42,50 +49,93 @@ export function startAgentHarnessRun(opts) {
42
49
  ownerEmail: opts.ownerEmail ?? opts.createSession?.ownerEmail ?? null,
43
50
  orgId: opts.orgId ?? opts.createSession?.orgId ?? null,
44
51
  });
52
+ harnessSessionRegistered = true;
45
53
  const input = {
46
54
  ...opts.input,
47
- abortSignal: signal,
55
+ abortSignal: runControl.chunkSignal,
48
56
  };
57
+ let firstStream = true;
49
58
  let pendingApproval = null;
50
- for await (const event of harnessSession.streamTurn(input)) {
51
- if (signal.aborted)
52
- break;
53
- await opts.onHarnessEvent?.(event);
54
- if (event.type === "approval-request") {
55
- pendingApproval = event;
56
- await updateAgentHarnessSession(storedSessionId, {
57
- status: "idle",
58
- pendingApproval: event,
59
+ while (true) {
60
+ pendingApproval = null;
61
+ const events = firstStream
62
+ ? harnessSession.streamTurn(input)
63
+ : harnessSession.continueTurn({
64
+ abortSignal: runControl.chunkSignal,
59
65
  });
66
+ firstStream = false;
67
+ try {
68
+ for await (const event of events) {
69
+ if (runControl.turnSignal.aborted)
70
+ break;
71
+ await opts.onHarnessEvent?.(event);
72
+ if (event.type === "approval-request") {
73
+ pendingApproval = event;
74
+ await updateAgentHarnessSession(storedSessionId, {
75
+ status: "idle",
76
+ pendingApproval: event,
77
+ });
78
+ }
79
+ if (event.type === "error") {
80
+ throw new Error(event.error);
81
+ }
82
+ for (const chatEvent of agentHarnessEventToAgentChatEvents(event)) {
83
+ send(chatEvent);
84
+ }
85
+ }
86
+ }
87
+ catch (error) {
88
+ if (runControl.turnSignal.aborted ||
89
+ !runControl.chunkBoundaryReason()) {
90
+ throw error;
91
+ }
92
+ }
93
+ if (runControl.turnSignal.aborted) {
94
+ await stopHarnessSession(harnessSession);
95
+ releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
96
+ await markAgentHarnessSessionStopped(storedSessionId, "stopped");
97
+ return;
98
+ }
99
+ if (pendingApproval) {
100
+ const stored = await getAgentHarnessSession(storedSessionId);
101
+ const stillPending = stored?.pendingApproval &&
102
+ typeof stored.pendingApproval === "object" &&
103
+ "id" in stored.pendingApproval &&
104
+ stored.pendingApproval.id === pendingApproval.id;
105
+ if (stillPending) {
106
+ keepLiveSession = true;
107
+ await updateAgentHarnessSession(storedSessionId, {
108
+ status: "idle",
109
+ pendingApproval,
110
+ });
111
+ return;
112
+ }
60
113
  }
61
- if (event.type === "error") {
62
- throw new Error(event.error);
114
+ if (!runControl.chunkBoundaryReason())
115
+ break;
116
+ if (!harnessSession.continueTurn) {
117
+ await saveHarnessCheckpoint(storedSessionId, harnessSession, opts.createSession?.resumeState, detachOnComplete);
118
+ if (detachOnComplete) {
119
+ releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
120
+ }
121
+ else {
122
+ keepLiveSession = true;
123
+ }
124
+ return;
63
125
  }
64
- for (const chatEvent of agentHarnessEventToAgentChatEvents(event)) {
65
- send(chatEvent);
126
+ if (runControl.beginChunk().aborted) {
127
+ await stopHarnessSession(harnessSession);
128
+ releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
129
+ await markAgentHarnessSessionStopped(storedSessionId, "stopped");
130
+ return;
66
131
  }
67
132
  }
68
- if (signal.aborted) {
133
+ if (runControl.turnSignal.aborted) {
69
134
  await stopHarnessSession(harnessSession);
70
135
  releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
71
136
  await markAgentHarnessSessionStopped(storedSessionId, "stopped");
72
137
  return;
73
138
  }
74
- if (pendingApproval) {
75
- const stored = await getAgentHarnessSession(storedSessionId);
76
- const stillPending = stored?.pendingApproval &&
77
- typeof stored.pendingApproval === "object" &&
78
- "id" in stored.pendingApproval &&
79
- stored.pendingApproval.id === pendingApproval.id;
80
- if (stillPending) {
81
- keepLiveSession = true;
82
- await updateAgentHarnessSession(storedSessionId, {
83
- status: "idle",
84
- pendingApproval,
85
- });
86
- return;
87
- }
88
- }
89
139
  let resumeState = opts.createSession?.resumeState;
90
140
  if (detachOnComplete && harnessSession.detach) {
91
141
  resumeState = await harnessSession.detach();
@@ -98,6 +148,24 @@ export function startAgentHarnessRun(opts) {
98
148
  });
99
149
  }
100
150
  catch (error) {
151
+ if (isAgentHarnessSessionConflictError(error)) {
152
+ if (!harnessSessionRegistered) {
153
+ await stopHarnessSession(harnessSession);
154
+ return;
155
+ }
156
+ keepLiveSession = true;
157
+ const latest = storedSessionId
158
+ ? await getAgentHarnessSession(storedSessionId)
159
+ : null;
160
+ const terminal = latest?.status === "stopped" ||
161
+ latest?.status === "errored" ||
162
+ latest?.status === "destroyed";
163
+ keepLiveSession = !terminal;
164
+ if (terminal && storedSessionId) {
165
+ releaseLiveAgentHarnessSession(storedSessionId, harnessSession);
166
+ }
167
+ return;
168
+ }
101
169
  if (harnessSession && !keepLiveSession) {
102
170
  await stopHarnessSession(harnessSession).catch(() => undefined);
103
171
  }
@@ -118,6 +186,8 @@ export function startAgentHarnessRun(opts) {
118
186
  }, opts.onRunComplete, {
119
187
  ...(opts.runOptions ?? {}),
120
188
  turnId: opts.turnId ?? opts.runOptions?.turnId,
189
+ recoverChunkBoundaries: opts.runOptions?.recoverChunkBoundaries ?? true,
190
+ useHostedSoftTimeoutDefault: opts.runOptions?.useHostedSoftTimeoutDefault ?? true,
121
191
  // A harness adapter (e.g. Claude Code, Codex) owns its own model
122
192
  // selection internally and does not expose it here, so `model` is
123
193
  // left for the caller to supply via `runOptions` if it knows one.
@@ -125,6 +195,17 @@ export function startAgentHarnessRun(opts) {
125
195
  engineName: opts.runOptions?.engineName ?? opts.adapter.name,
126
196
  });
127
197
  }
198
+ async function saveHarnessCheckpoint(sessionId, session, resumeState, detachOnComplete) {
199
+ let nextResumeState = resumeState;
200
+ if (detachOnComplete && session.detach) {
201
+ nextResumeState = await session.detach();
202
+ }
203
+ await updateAgentHarnessSession(sessionId, {
204
+ status: "idle",
205
+ resumeState: nextResumeState,
206
+ pendingApproval: null,
207
+ });
208
+ }
128
209
  async function stopHarnessSession(session) {
129
210
  if (!session)
130
211
  return;
@@ -1,4 +1,8 @@
1
1
  export type AgentHarnessSessionStatus = "running" | "idle" | "stopped" | "errored" | "destroyed";
2
+ export declare class AgentHarnessSessionConflictError extends Error {
3
+ constructor(sessionId: string);
4
+ }
5
+ export declare function isAgentHarnessSessionConflictError(error: unknown): error is AgentHarnessSessionConflictError;
2
6
  export interface StoredAgentHarnessSession {
3
7
  id: string;
4
8
  harnessName: string;
@@ -12,6 +16,7 @@ export interface StoredAgentHarnessSession {
12
16
  resolvedApprovalIds?: string[];
13
17
  ownerEmail?: string | null;
14
18
  orgId?: string | null;
19
+ generation: number;
15
20
  createdAt: number;
16
21
  updatedAt: number;
17
22
  stoppedAt?: number | null;
@@ -30,6 +35,10 @@ export interface SaveAgentHarnessSessionInput {
30
35
  ownerEmail?: string | null;
31
36
  orgId?: string | null;
32
37
  stoppedAt?: number | null;
38
+ /** Internal optimistic-concurrency token used by updateAgentHarnessSession. */
39
+ expectedGeneration?: number;
40
+ /** Snapshot used by updateAgentHarnessSession so merge and CAS share a read. */
41
+ existingSession?: StoredAgentHarnessSession | null;
33
42
  }
34
43
  export declare function ensureAgentHarnessSessionTables(): Promise<void>;
35
44
  export declare function saveAgentHarnessSession(input: SaveAgentHarnessSessionInput): Promise<StoredAgentHarnessSession>;
@@ -1,5 +1,16 @@
1
1
  import { getDbExec, intType, isPostgres } from "../../db/client.js";
2
2
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../../db/ddl-guard.js";
3
+ export class AgentHarnessSessionConflictError extends Error {
4
+ constructor(sessionId) {
5
+ super(`Harness session ${sessionId} changed concurrently; retry the operation.`);
6
+ this.name = "AgentHarnessSessionConflictError";
7
+ }
8
+ }
9
+ export function isAgentHarnessSessionConflictError(error) {
10
+ return (error instanceof AgentHarnessSessionConflictError ||
11
+ (error instanceof Error &&
12
+ error.name === "AgentHarnessSessionConflictError"));
13
+ }
3
14
  let initPromise;
4
15
  export async function ensureAgentHarnessSessionTables() {
5
16
  if (!initPromise) {
@@ -19,6 +30,7 @@ export async function ensureAgentHarnessSessionTables() {
19
30
  resolved_approval_ids TEXT,
20
31
  owner_email TEXT,
21
32
  org_id TEXT,
33
+ generation ${intType()} NOT NULL DEFAULT 0,
22
34
  created_at ${intType()} NOT NULL,
23
35
  updated_at ${intType()} NOT NULL,
24
36
  stopped_at ${intType()}
@@ -41,6 +53,7 @@ export async function ensureAgentHarnessSessionTables() {
41
53
  await ensureColumnExists("agent_harness_sessions", col, `ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS ${col} TEXT`);
42
54
  }
43
55
  await ensureColumnExists("agent_harness_sessions", "stopped_at", `ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS stopped_at ${intType()}`);
56
+ await ensureColumnExists("agent_harness_sessions", "generation", `ALTER TABLE agent_harness_sessions ADD COLUMN IF NOT EXISTS generation ${intType()} NOT NULL DEFAULT 0`);
44
57
  await ensureIndexExists("idx_agent_harness_sessions_thread", `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_thread ON agent_harness_sessions(thread_id, updated_at)`);
45
58
  await ensureIndexExists("idx_agent_harness_sessions_status", `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_status ON agent_harness_sessions(status, updated_at)`);
46
59
  await ensureIndexExists("idx_agent_harness_sessions_owner", `CREATE INDEX IF NOT EXISTS idx_agent_harness_sessions_owner ON agent_harness_sessions(owner_email, updated_at)`);
@@ -48,6 +61,13 @@ export async function ensureAgentHarnessSessionTables() {
48
61
  }
49
62
  // SQLite (local dev): no lock problem — keep the original behaviour.
50
63
  await client.execute(createSql);
64
+ try {
65
+ await client.execute(`ALTER TABLE agent_harness_sessions ADD COLUMN generation ${intType()} NOT NULL DEFAULT 0`);
66
+ }
67
+ catch (error) {
68
+ if (!isDuplicateColumnError(error))
69
+ throw error;
70
+ }
51
71
  for (const col of [
52
72
  "run_id",
53
73
  "provider_session_id",
@@ -99,7 +119,9 @@ export async function ensureAgentHarnessSessionTables() {
99
119
  export async function saveAgentHarnessSession(input) {
100
120
  await ensureAgentHarnessSessionTables();
101
121
  const now = Date.now();
102
- const existing = await getAgentHarnessSession(input.id);
122
+ const existing = input.existingSession === undefined
123
+ ? await getAgentHarnessSession(input.id)
124
+ : input.existingSession;
103
125
  const record = {
104
126
  id: input.id,
105
127
  harnessName: input.harnessName,
@@ -117,33 +139,60 @@ export async function saveAgentHarnessSession(input) {
117
139
  resolvedApprovalIds: input.resolvedApprovalIds ?? existing?.resolvedApprovalIds ?? [],
118
140
  ownerEmail: input.ownerEmail ?? existing?.ownerEmail ?? null,
119
141
  orgId: input.orgId ?? existing?.orgId ?? null,
142
+ generation: (input.expectedGeneration ?? existing?.generation ?? 0) + 1,
120
143
  createdAt: existing?.createdAt ?? now,
121
144
  updatedAt: now,
122
145
  stoppedAt: input.stoppedAt ?? existing?.stoppedAt ?? null,
123
146
  };
124
147
  const client = getDbExec();
125
- await client.execute({
126
- sql: `INSERT INTO agent_harness_sessions (
127
- id, harness_name, thread_id, run_id, provider_session_id, status,
128
- resume_state, workspace_ref, pending_approval, resolved_approval_ids,
129
- owner_email, org_id, created_at, updated_at, stopped_at
130
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
131
- ON CONFLICT(id) DO UPDATE SET
132
- harness_name = excluded.harness_name,
133
- thread_id = excluded.thread_id,
134
- run_id = excluded.run_id,
135
- provider_session_id = excluded.provider_session_id,
136
- status = excluded.status,
137
- resume_state = excluded.resume_state,
138
- workspace_ref = excluded.workspace_ref,
139
- pending_approval = excluded.pending_approval,
140
- resolved_approval_ids = excluded.resolved_approval_ids,
141
- owner_email = excluded.owner_email,
142
- org_id = excluded.org_id,
143
- updated_at = excluded.updated_at,
144
- stopped_at = excluded.stopped_at`,
148
+ const values = [
149
+ record.id,
150
+ record.harnessName,
151
+ record.threadId,
152
+ record.runId ?? null,
153
+ record.providerSessionId ?? null,
154
+ record.status,
155
+ serializeJson(record.resumeState),
156
+ record.workspaceRef ?? null,
157
+ serializeJson(record.pendingApproval),
158
+ serializeJson(record.resolvedApprovalIds),
159
+ record.ownerEmail ?? null,
160
+ record.orgId ?? null,
161
+ record.generation,
162
+ record.createdAt,
163
+ record.updatedAt,
164
+ record.stoppedAt ?? null,
165
+ ];
166
+ if (!existing) {
167
+ const inserted = await client.execute({
168
+ sql: `INSERT INTO agent_harness_sessions (
169
+ id, harness_name, thread_id, run_id, provider_session_id, status,
170
+ resume_state, workspace_ref, pending_approval, resolved_approval_ids,
171
+ owner_email, org_id, generation, created_at, updated_at, stopped_at
172
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
173
+ ON CONFLICT(id) DO NOTHING`,
174
+ args: values,
175
+ });
176
+ if (inserted.rowsAffected === 1)
177
+ return record;
178
+ const current = await getAgentHarnessSession(input.id);
179
+ if (!current)
180
+ throw new Error("Harness session insert did not persist.");
181
+ return saveAgentHarnessSession({
182
+ ...input,
183
+ expectedGeneration: undefined,
184
+ existingSession: undefined,
185
+ });
186
+ }
187
+ const expectedGeneration = input.expectedGeneration ?? existing.generation;
188
+ const updated = await client.execute({
189
+ sql: `UPDATE agent_harness_sessions SET
190
+ harness_name = ?, thread_id = ?, run_id = ?, provider_session_id = ?,
191
+ status = ?, resume_state = ?, workspace_ref = ?, pending_approval = ?,
192
+ resolved_approval_ids = ?, owner_email = ?, org_id = ?, generation = ?,
193
+ updated_at = ?, stopped_at = ?
194
+ WHERE id = ? AND generation = ?`,
145
195
  args: [
146
- record.id,
147
196
  record.harnessName,
148
197
  record.threadId,
149
198
  record.runId ?? null,
@@ -155,11 +204,16 @@ export async function saveAgentHarnessSession(input) {
155
204
  serializeJson(record.resolvedApprovalIds),
156
205
  record.ownerEmail ?? null,
157
206
  record.orgId ?? null,
158
- record.createdAt,
207
+ record.generation,
159
208
  record.updatedAt,
160
209
  record.stoppedAt ?? null,
210
+ record.id,
211
+ expectedGeneration,
161
212
  ],
162
213
  });
214
+ if (updated.rowsAffected !== 1) {
215
+ throw new AgentHarnessSessionConflictError(record.id);
216
+ }
163
217
  return record;
164
218
  }
165
219
  export async function updateAgentHarnessSession(id, patch) {
@@ -184,6 +238,8 @@ export async function updateAgentHarnessSession(id, patch) {
184
238
  ownerEmail: patch.ownerEmail ?? existing.ownerEmail ?? null,
185
239
  orgId: patch.orgId ?? existing.orgId ?? null,
186
240
  stoppedAt: patch.stoppedAt ?? existing.stoppedAt ?? null,
241
+ expectedGeneration: existing.generation,
242
+ existingSession: existing,
187
243
  });
188
244
  }
189
245
  export async function getAgentHarnessSession(id) {
@@ -298,6 +354,7 @@ function rowToHarnessSession(row) {
298
354
  resolvedApprovalIds: stringArray(record.resolved_approval_ids),
299
355
  ownerEmail: stringOrNull(record.owner_email),
300
356
  orgId: stringOrNull(record.org_id),
357
+ generation: numberValue(record.generation),
301
358
  createdAt: numberValue(record.created_at),
302
359
  updatedAt: numberValue(record.updated_at),
303
360
  stoppedAt: record.stopped_at === null || record.stopped_at === undefined
@@ -344,3 +401,10 @@ function stringOrNull(value) {
344
401
  function numberValue(value) {
345
402
  return typeof value === "number" ? value : Number(value ?? 0);
346
403
  }
404
+ function isDuplicateColumnError(error) {
405
+ if (!error || typeof error !== "object")
406
+ return false;
407
+ const message = error.message;
408
+ return (typeof message === "string" &&
409
+ /duplicate column name|column .* already exists/i.test(message));
410
+ }
@@ -730,6 +730,7 @@ export interface ExecuteAgentToolCallOptions {
730
730
  approvedToolCalls?: string[];
731
731
  onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<string | void>;
732
732
  consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
733
+ isToolAlwaysAllowed?: (binding: AgentApprovalBinding) => Promise<boolean>;
733
734
  send?: (event: AgentChatEvent) => void;
734
735
  }
735
736
  export interface AgentApprovalBinding {
@@ -915,6 +916,8 @@ export declare function runAgentLoop(opts: {
915
916
  */
916
917
  onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<string | void>;
917
918
  consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
919
+ /** User-scoped action-type policy, checked only after needsApproval. */
920
+ isToolAlwaysAllowed?: (binding: AgentApprovalBinding) => Promise<boolean>;
918
921
  /**
919
922
  * In-loop processor seam (see `processors.ts`). Each processor can observe
920
923
  * streamed chunks, observe model responses around tool execution, and
@@ -43,7 +43,7 @@ import { ProcessorChain, TripWire, toolCallsFromContent, } from "./processors.js
43
43
  import { startRun, subscribeToRun, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, abortTurnDurably, tryClaimRunSlot, isHostedRuntime, resolveRunSoftTimeoutMs, resolveRunToolTimeoutCeilingMs, endsAfterCompletedToolWithoutAssistantFinal, } from "./run-manager.js";
44
44
  import { writeLedgerEntry, readLedgerEntry, clearLedgerForThread, insertRun, insertRunEvent, isTurnAborted, markRunAborted, updateRunHeartbeat, updateRunStatusIfRunning, setRunError, setRunTerminalReason, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, countRunsForTurn, RUN_DIAG_STAGE, UNCLAIMED_BACKGROUND_RUN_GRACE_MS, turnRunLedgerExhausted, } from "./run-store.js";
45
45
  import { buildCurrentTimeUserContext } from "./runtime-context.js";
46
- import { consumeAgentToolApproval, createAgentToolApproval, resolveAgentToolApprovalTurnId, } from "./tool-approval-store.js";
46
+ import { consumeAgentToolApproval, createAgentToolApproval, isAgentToolAlwaysAllowed, resolveAgentToolApprovalTurnId, } from "./tool-approval-store.js";
47
47
  import { findCompletedJournalEntry, } from "./tool-call-journal.js";
48
48
  import { redactSensitiveFields, sanitizeToolErrorText, sanitizeToolErrorValue, } from "./tool-error-redaction.js";
49
49
  import { describeToolResultImages, extractAgentImagesFromActionResult, } from "./tool-result-images.js";
@@ -2274,6 +2274,7 @@ export async function executeAgentToolCall(options) {
2274
2274
  approvedToolCalls: options.approvedToolCalls,
2275
2275
  onApprovalRequired: options.onApprovalRequired,
2276
2276
  consumeApproval: options.consumeApproval,
2277
+ isToolAlwaysAllowed: options.isToolAlwaysAllowed,
2277
2278
  });
2278
2279
  }
2279
2280
  catch (error) {
@@ -4359,6 +4360,16 @@ export async function runAgentLoop(opts) {
4359
4360
  // than silently running a high-consequence action.
4360
4361
  mustApprove = true;
4361
4362
  }
4363
+ if (mustApprove && opts.isToolAlwaysAllowed) {
4364
+ try {
4365
+ mustApprove = !(await opts.isToolAlwaysAllowed(approvalBinding));
4366
+ }
4367
+ catch {
4368
+ // Fail closed: an unreadable policy must leave the approval gate in
4369
+ // place instead of turning a storage outage into authorization.
4370
+ mustApprove = true;
4371
+ }
4372
+ }
4362
4373
  if (mustApprove) {
4363
4374
  // `askId` identifies THIS gate hit, distinct from any earlier ask
4364
4375
  // for the same approvalKey/toolCallId. A failed resume (expired
@@ -7226,6 +7237,15 @@ export function createProductionAgentHandler(options) {
7226
7237
  return false;
7227
7238
  return consumeAgentToolApproval(approvalStoreBinding(binding));
7228
7239
  },
7240
+ isToolAlwaysAllowed: async (binding) => {
7241
+ if (!ownerEmail)
7242
+ return false;
7243
+ return isAgentToolAlwaysAllowed({
7244
+ ownerEmail,
7245
+ orgId: getRequestOrgId() ?? null,
7246
+ toolName: binding.toolName,
7247
+ });
7248
+ },
7229
7249
  };
7230
7250
  const approvedToolCallsForExecution = requestedApprovedToolCalls;
7231
7251
  if (isBackgroundWorker &&
@@ -8074,6 +8094,7 @@ export function createProductionAgentHandler(options) {
8074
8094
  : {}),
8075
8095
  onApprovalRequired: approvalHooks.onApprovalRequired,
8076
8096
  consumeApproval: approvalHooks.consumeApproval,
8097
+ isToolAlwaysAllowed: approvalHooks.isToolAlwaysAllowed,
8077
8098
  // A worker PROVEN to be running inside the real 15-min Netlify
8078
8099
  // `-background` function (`runsInBackgroundFunction`) has minutes of
8079
8100
  // budget left on THIS invocation. Let it catch a recoverable
@@ -8119,6 +8140,7 @@ export function createProductionAgentHandler(options) {
8119
8140
  approvedToolCalls: approvedToolCallsForExecution,
8120
8141
  onApprovalRequired: approvalHooks.onApprovalRequired,
8121
8142
  consumeApproval: approvalHooks.consumeApproval,
8143
+ isToolAlwaysAllowed: approvalHooks.isToolAlwaysAllowed,
8122
8144
  send,
8123
8145
  });
8124
8146
  return;
@@ -663,6 +663,9 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
663
663
  ? new AbortController()
664
664
  : null;
665
665
  let chunkBoundaryReason = null;
666
+ let chunkSoftTimeoutTimer = null;
667
+ let armChunkSoftTimeout = () => { };
668
+ let recoverableRunDeadlineAt = null;
666
669
  let recoveredChunkBoundaries = 0;
667
670
  /** A boundary that has been reached but not yet proven recovered. */
668
671
  let pendingBoundary = null;
@@ -706,7 +709,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
706
709
  return abort.signal;
707
710
  settleBoundary(true);
708
711
  chunkBoundaryReason = null;
712
+ if (chunkSoftTimeoutTimer)
713
+ clearTimeout(chunkSoftTimeoutTimer);
709
714
  chunkAbort = new AbortController();
715
+ armChunkSoftTimeout();
710
716
  // The boundary is behind us; the silence clock restarts with the chunk,
711
717
  // or the backstop fires again on the elapsed time of the chunk it just
712
718
  // ended and every recovery round dies instantly.
@@ -1079,8 +1085,12 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1079
1085
  const reachRunBoundary = (reason, diagnostic = {}) => {
1080
1086
  if (run.status !== "running" || abort.signal.aborted)
1081
1087
  return;
1082
- if (chunkAbort) {
1083
- if (chunkAbort.signal.aborted)
1088
+ const activeChunkAbort = chunkAbort;
1089
+ const canRecoverChunk = activeChunkAbort !== null &&
1090
+ (recoverableRunDeadlineAt === null ||
1091
+ Date.now() < recoverableRunDeadlineAt);
1092
+ if (canRecoverChunk) {
1093
+ if (activeChunkAbort.signal.aborted)
1084
1094
  return;
1085
1095
  recoveredChunkBoundaries += 1;
1086
1096
  console.warn(`[run-manager] chunk boundary (${reason}) — recovering in-invocation`, runId, diagnostic);
@@ -1092,9 +1102,11 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1092
1102
  // direction that hides the failure.
1093
1103
  pendingBoundary = { reason, diagnostic, index: recoveredChunkBoundaries };
1094
1104
  chunkBoundaryReason = reason;
1095
- chunkAbort.abort(reason);
1105
+ activeChunkAbort.abort(reason);
1096
1106
  return;
1097
1107
  }
1108
+ if (chunkAbort)
1109
+ chunkBoundaryReason = reason;
1098
1110
  // Mirror the soft-timeout semantics exactly: the chunk completes (not
1099
1111
  // aborts) at an auto_continue boundary, so the continuation machinery —
1100
1112
  // server-chained for background workers, client-driven for foreground —
@@ -1277,12 +1289,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1277
1289
  overrideMs: options?.noProgressTimeoutMs,
1278
1290
  backgroundOverrideMs: options?.backgroundNoProgressTimeoutMs,
1279
1291
  });
1280
- // Not armed for a runFn that recovers boundaries in this invocation. That
1281
- // runFn already races the SAME wall with its own per-round timer, budgeted
1282
- // against cumulative elapsed time — so this timer fires at the moment the
1283
- // wrapper has nothing left to continue with, producing a boundary that is
1284
- // recoverable in name only and burning the tail of the budget on nothing.
1285
- // One wall, one clock; the caller's hard abort still backstops it.
1292
+ // A recoverable run uses the cumulative timer below instead of this one-shot
1293
+ // timer. Each successor chunk gets only the remaining invocation budget, so
1294
+ // recovery cannot extend the hosted wall. The caller's hard abort still
1295
+ // backstops both timer paths.
1286
1296
  const softTimeoutTimer = softTimeoutMs > 0 && !recoverChunkBoundaries
1287
1297
  ? setTimeout(() => {
1288
1298
  reachRunBoundary("run_timeout", {
@@ -1290,6 +1300,25 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1290
1300
  });
1291
1301
  }, softTimeoutMs)
1292
1302
  : null;
1303
+ if (recoverChunkBoundaries && softTimeoutMs > 0) {
1304
+ const runDeadlineAt = Date.now() + softTimeoutMs;
1305
+ recoverableRunDeadlineAt = runDeadlineAt;
1306
+ armChunkSoftTimeout = () => {
1307
+ if (chunkSoftTimeoutTimer)
1308
+ clearTimeout(chunkSoftTimeoutTimer);
1309
+ if (abort.signal.aborted || run.status !== "running")
1310
+ return;
1311
+ chunkSoftTimeoutTimer = setTimeout(() => {
1312
+ chunkSoftTimeoutTimer = null;
1313
+ reachRunBoundary("run_timeout", {
1314
+ lastEventType: run.events.at(-1)?.event.type,
1315
+ });
1316
+ }, Math.max(0, runDeadlineAt - Date.now()));
1317
+ };
1318
+ // Boundary recovery disables the ordinary one-shot timer, so arm the
1319
+ // cumulative invocation deadline on the initial chunk and each successor.
1320
+ armChunkSoftTimeout();
1321
+ }
1293
1322
  let pendingTerminalEvent = null;
1294
1323
  const captureRunError = (error, phase) => {
1295
1324
  const errorCode = getRunErrorCode(error);
@@ -1638,6 +1667,8 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1638
1667
  clearInterval(heartbeatTimer);
1639
1668
  if (softTimeoutTimer)
1640
1669
  clearTimeout(softTimeoutTimer);
1670
+ if (chunkSoftTimeoutTimer)
1671
+ clearTimeout(chunkSoftTimeoutTimer);
1641
1672
  if (progressWriteTimer)
1642
1673
  clearTimeout(progressWriteTimer);
1643
1674
  progressWriteTimer = null;