@agent-native/core 0.210.0 → 0.210.1-nightly-20261020010058

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 (82) hide show
  1. package/dist/action.js +16 -1
  2. package/dist/agent/production-agent.d.ts +1 -0
  3. package/dist/agent/production-agent.js +2 -0
  4. package/dist/agent/reaped-run-thread.d.ts +1 -1
  5. package/dist/agent/reaped-run-thread.js +21 -6
  6. package/dist/agent/run-store.fencing.worker.js +58 -0
  7. package/dist/agent/run-store.js +7 -5
  8. package/dist/agent/run-thread-projector.d.ts +27 -0
  9. package/dist/agent/run-thread-projector.js +79 -0
  10. package/dist/agent/thread-data-builder.js +38 -13
  11. package/dist/automation/index.d.ts +1 -1
  12. package/dist/cli/create.d.ts +1 -1
  13. package/dist/cli/create.js +30 -27
  14. package/dist/cli/package-lifecycle.js +3 -2
  15. package/dist/cli/setup-agents.d.ts +5 -0
  16. package/dist/cli/setup-agents.js +91 -34
  17. package/dist/cli/skills.js +8 -14
  18. package/dist/cli/tar-command.d.ts +1 -0
  19. package/dist/cli/tar-command.js +9 -0
  20. package/dist/cli/template-baseline.js +6 -3
  21. package/dist/cli/template-sync.d.ts +2 -1
  22. package/dist/cli/template-sync.js +2 -1
  23. package/dist/client/agent-chat.d.ts +1 -0
  24. package/dist/client/agent-chat.js +1 -1
  25. package/dist/client/api-path.d.ts +1 -0
  26. package/dist/client/api-path.js +6 -0
  27. package/dist/client/chat/agentkit-agent-native.js +6 -2
  28. package/dist/client/chat/runtime.js +3 -0
  29. package/dist/client/use-agent-chat-context.js +6 -3
  30. package/dist/client/use-db-sync.js +3 -0
  31. package/dist/collab/awareness.d.ts +2 -2
  32. package/dist/collab/routes.d.ts +2 -2
  33. package/dist/deploy/immutable-assets.d.ts +1 -1
  34. package/dist/deploy/immutable-assets.js +1 -1
  35. package/dist/deploy/workspace-deploy.js +12 -5
  36. package/dist/file-upload/actions/upload-image.d.ts +3 -3
  37. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  38. package/dist/jobs/background-automation-runner.js +16 -7
  39. package/dist/mcp/build-server.js +5 -1
  40. package/dist/mcp/embed-app.js +84 -17
  41. package/dist/notifications/routes.d.ts +3 -3
  42. package/dist/observability/routes.d.ts +7 -7
  43. package/dist/org/actions/invite-organization-member.js +1 -0
  44. package/dist/org/actions/list-organization-members.js +1 -0
  45. package/dist/org/index.d.ts +1 -1
  46. package/dist/progress/routes.d.ts +1 -1
  47. package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
  48. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  49. package/dist/scripts/db/exec.js +18 -6
  50. package/dist/scripts/db/patch.js +20 -8
  51. package/dist/scripts/db/postgres-client.d.ts +2 -0
  52. package/dist/scripts/db/postgres-client.js +17 -1
  53. package/dist/scripts/db/query.js +7 -6
  54. package/dist/scripts/db/safety.js +2 -1
  55. package/dist/secrets/routes.d.ts +10 -10
  56. package/dist/server/agent-chat-plugin.js +17 -8
  57. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  58. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  59. package/dist/server/agent-teams.js +9 -3
  60. package/dist/server/framework-request-handler.js +19 -0
  61. package/dist/server/fusion-app.d.ts +7 -0
  62. package/dist/server/fusion-app.js +11 -2
  63. package/dist/templates/builder-code-starter/.agents/skills/actions/SKILL.md.patch +1 -1
  64. package/dist/templates/builder-code-starter/app/hooks/use-navigation-state.ts.patch +26 -45
  65. package/dist/templates/builder-code-starter/server/plugins/agent-chat.ts.patch +2 -1
  66. package/dist/templates/chat/.agents/skills/context-awareness/SKILL.md +5 -0
  67. package/dist/templates/chat/app/components/chat/ChatHomeRedirect.tsx +2 -2
  68. package/dist/templates/chat/app/hooks/use-navigation-state.ts +51 -22
  69. package/dist/templates/chat/server/plugins/agent-chat.ts +1 -0
  70. package/dist/templates/default/.agents/skills/context-awareness/SKILL.md +5 -0
  71. package/dist/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +5 -0
  72. package/dist/triggers/actions/manage-automation.d.ts +5 -5
  73. package/package.json +5 -5
  74. package/src/templates/builder-code-starter/.agents/skills/actions/SKILL.md.patch +1 -1
  75. package/src/templates/builder-code-starter/app/hooks/use-navigation-state.ts.patch +26 -45
  76. package/src/templates/builder-code-starter/server/plugins/agent-chat.ts.patch +2 -1
  77. package/src/templates/chat/.agents/skills/context-awareness/SKILL.md +5 -0
  78. package/src/templates/chat/app/components/chat/ChatHomeRedirect.tsx +2 -2
  79. package/src/templates/chat/app/hooks/use-navigation-state.ts +51 -22
  80. package/src/templates/chat/server/plugins/agent-chat.ts +1 -0
  81. package/src/templates/default/.agents/skills/context-awareness/SKILL.md +5 -0
  82. package/src/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +5 -0
package/dist/action.js CHANGED
@@ -1026,7 +1026,22 @@ function zodDefToJsonSchema(def) {
1026
1026
  return result;
1027
1027
  }
1028
1028
  if (type === "literal") {
1029
- return { type: typeof def.value, enum: [def.value] };
1029
+ const values = Array.isArray(def.values)
1030
+ ? def.values
1031
+ : def.values instanceof Set
1032
+ ? [...def.values]
1033
+ : [def.value];
1034
+ const jsonTypeOf = (value) => value === null ? "null" : typeof value;
1035
+ const uniqueTypes = [...new Set(values.map(jsonTypeOf))];
1036
+ if (uniqueTypes.length === 1) {
1037
+ return { type: uniqueTypes[0], enum: values };
1038
+ }
1039
+ return {
1040
+ anyOf: values.map((value) => ({
1041
+ type: jsonTypeOf(value),
1042
+ enum: [value],
1043
+ })),
1044
+ };
1030
1045
  }
1031
1046
  if (type === "array") {
1032
1047
  const result = { type: "array" };
@@ -631,6 +631,7 @@ export interface ExecuteAgentToolCallOptions {
631
631
  onApprovalRequired?: (binding: AgentApprovalBinding) => Promise<string | void>;
632
632
  consumeApproval?: (binding: AgentApprovalBinding) => Promise<boolean>;
633
633
  isToolAlwaysAllowed?: (binding: AgentApprovalBinding) => Promise<boolean>;
634
+ beforeWrite?: () => Promise<void>;
634
635
  send?: (event: AgentChatEvent) => void;
635
636
  }
636
637
  export interface AgentApprovalBinding {
@@ -3427,6 +3427,7 @@ export async function executeAgentToolCall(options) {
3427
3427
  onApprovalRequired: options.onApprovalRequired,
3428
3428
  consumeApproval: options.consumeApproval,
3429
3429
  isToolAlwaysAllowed: options.isToolAlwaysAllowed,
3430
+ beforeWrite: options.beforeWrite,
3430
3431
  });
3431
3432
  }
3432
3433
  catch (error) {
@@ -11050,6 +11051,7 @@ export function createProductionAgentHandler(options) {
11050
11051
  onApprovalRequired: approvalHooks.onApprovalRequired,
11051
11052
  consumeApproval: approvalHooks.consumeApproval,
11052
11053
  isToolAlwaysAllowed: approvalHooks.isToolAlwaysAllowed,
11054
+ beforeWrite: agentLoopOpts.beforeWrite,
11053
11055
  send,
11054
11056
  });
11055
11057
  if (approvedExecution.status === "approval_required" ||
@@ -13,4 +13,4 @@
13
13
  export declare function foldReapedRunIntoThread(runId: string, terminalError?: {
14
14
  error: string;
15
15
  errorCode: string;
16
- }): Promise<void>;
16
+ }): Promise<"projected" | "superseded" | "retry">;
@@ -1,4 +1,5 @@
1
1
  import { getThread, updateThreadData, withThreadDataLock, } from "../chat-threads/store.js";
2
+ import { RUN_NOT_STARTED_METADATA_KEY } from "../shared/agent-chat-run-not-started.js";
2
3
  import { readStoppedRunForThreadFold } from "./run-store.js";
3
4
  import { buildAssistantMessage, extractThreadMeta, foldAssistantTurn, isRunPrompt, normalizeThreadRepository, } from "./thread-data-builder.js";
4
5
  /**
@@ -16,7 +17,7 @@ import { buildAssistantMessage, extractThreadMeta, foldAssistantTurn, isRunPromp
16
17
  export async function foldReapedRunIntoThread(runId, terminalError) {
17
18
  const run = await readStoppedRunForThreadFold(runId);
18
19
  if (!run || run.laterTurnStarted)
19
- return;
20
+ return "superseded";
20
21
  const events = [...run.events];
21
22
  if (terminalError) {
22
23
  const last = events.at(-1);
@@ -45,18 +46,19 @@ export async function foldReapedRunIntoThread(runId, terminalError) {
45
46
  suppressInternalContinuation: run.continuedInTurn,
46
47
  });
47
48
  if (!assistant)
48
- return;
49
- await withThreadDataLock(run.threadId, async () => {
49
+ return "projected";
50
+ return withThreadDataLock(run.threadId, async () => {
50
51
  const thread = await getThread(run.threadId);
51
52
  if (!thread)
52
- return;
53
+ return "superseded";
53
54
  const repo = normalizeThreadRepository(JSON.parse(thread.threadData || "{}"));
54
55
  const folded = foldAssistantTurn(repo, assistant, {
55
56
  runId,
56
57
  turnId: run.turnId,
57
58
  });
58
59
  const meta = extractThreadMeta(folded);
59
- await updateThreadData(run.threadId, JSON.stringify(folded), thread.title, meta.preview || thread.preview, folded.messages.length, {
60
+ let rejectedDisposition = "superseded";
61
+ const updated = await updateThreadData(run.threadId, JSON.stringify(folded), thread.title, meta.preview || thread.preview, folded.messages.length, {
60
62
  beforeUpdate: async (tx) => {
61
63
  await tx.execute({
62
64
  sql: "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))",
@@ -88,11 +90,24 @@ export async function foldReapedRunIntoThread(runId, terminalError) {
88
90
  return false;
89
91
  const currentRepo = normalizeThreadRepository(JSON.parse(currentThreadData || "{}"));
90
92
  const last = currentRepo.messages.at(-1)?.message ?? currentRepo.messages.at(-1);
91
- return last?.role === "user"
93
+ const ownsPrompt = last?.role === "user"
92
94
  ? isRunPrompt(last, { runId, ...run })
93
95
  : last?.role === "assistant" &&
94
96
  last.metadata?.custom?.turnId === run.turnId;
97
+ const unstartedTurnId = last?.role === "user"
98
+ ? last.metadata?.custom?.submittedTurnId
99
+ : last?.role === "assistant"
100
+ ? last.metadata?.custom?.turnId
101
+ : undefined;
102
+ const isRefusedNewerTurn = last?.metadata?.custom?.[RUN_NOT_STARTED_METADATA_KEY] === true &&
103
+ typeof unstartedTurnId === "string" &&
104
+ unstartedTurnId !== run.turnId;
105
+ if (!ownsPrompt && !isRefusedNewerTurn) {
106
+ rejectedDisposition = "retry";
107
+ }
108
+ return ownsPrompt;
95
109
  },
96
110
  });
111
+ return updated ? "projected" : rejectedDisposition;
97
112
  });
98
113
  }
@@ -1,9 +1,11 @@
1
1
  import { writeSync } from "node:fs";
2
2
  import postgres from "postgres";
3
+ import { ensureChatThreadTables, getThread, markQueuedSubmissionSubmitted, updateThreadData, } from "../chat-threads/store.js";
3
4
  import { toPostgresParams, withDbExec } from "../db/client.js";
4
5
  import { withMigrationRuntime } from "../db/migration-runtime.js";
5
6
  import { withExecutionFence } from "./execution-fence.js";
6
7
  import { beginAgentExecutionStep, claimTurnExecutionLease, completeAgentExecutionStep, ensureRunTables, insertRun, insertRunEvent, markAgentExecutionStepDispatched, } from "./run-store.js";
8
+ import { applySubmittedUserMessage, buildUserMessage, extractThreadMeta, } from "./thread-data-builder.js";
7
9
  function executor(sql) {
8
10
  return {
9
11
  async execute(statement) {
@@ -54,6 +56,62 @@ async function main() {
54
56
  process.stdout.write(JSON.stringify({ status: "unexpected-write" }));
55
57
  return;
56
58
  }
59
+ if (mode === "queue-submit") {
60
+ await withMigrationRuntime(() => ensureChatThreadTables());
61
+ const input = JSON.parse(extra ?? "null");
62
+ const thread = await getThread(threadId);
63
+ if (!thread)
64
+ throw new Error(`Missing queued-message thread ${threadId}`);
65
+ const userMessage = buildUserMessage({
66
+ text: input.message.text,
67
+ runId,
68
+ queuedMessageId: input.message.id,
69
+ });
70
+ let submissionFailure;
71
+ let pauseBeforeWrite = true;
72
+ const updated = await updateThreadData(threadId, "{}", thread.title, thread.preview, thread.messageCount, {
73
+ transformThreadData: (threadData) => {
74
+ const result = applySubmittedUserMessage(JSON.parse(threadData || "{}"), userMessage, {
75
+ id: input.message.id,
76
+ claimId: input.claimId,
77
+ now: Date.now(),
78
+ });
79
+ if (!("repo" in result)) {
80
+ submissionFailure = result.status;
81
+ return threadData;
82
+ }
83
+ const meta = extractThreadMeta(result.repo);
84
+ return {
85
+ threadData: JSON.stringify(result.repo),
86
+ preview: meta.preview || thread.preview,
87
+ };
88
+ },
89
+ beforeUpdate: async () => {
90
+ if (pauseBeforeWrite) {
91
+ pauseBeforeWrite = false;
92
+ writeSync(1, `${JSON.stringify({ ready: true })}\n`);
93
+ await waitForContinue();
94
+ }
95
+ return !submissionFailure;
96
+ },
97
+ afterUpdate: async (tx) => {
98
+ if (submissionFailure)
99
+ return;
100
+ await markQueuedSubmissionSubmitted(tx, {
101
+ threadId,
102
+ messageId: input.message.id,
103
+ runId,
104
+ actor: input.actor,
105
+ message: input.message,
106
+ });
107
+ },
108
+ });
109
+ writeSync(1, JSON.stringify({
110
+ status: submissionFailure ?? "submitted",
111
+ updated,
112
+ }));
113
+ return;
114
+ }
57
115
  await insertRun(runId, threadId, turnId);
58
116
  const fence = await claimTurnExecutionLease(threadId, turnId, runId);
59
117
  if (mode === "claim") {
@@ -2123,10 +2123,10 @@ async function reapSingleStaleRun(runId, source, maxStaleMs) {
2123
2123
  args: [runScope.thread_id, runScope.turn_id, runId, lease.generation],
2124
2124
  });
2125
2125
  }
2126
- outcome =
2127
- lease && !generationMatches
2128
- ? { outcome: "execution_lease_changed" }
2129
- : await attemptStaleRunRecovery(tx, runId);
2126
+ const executionLeaseChanged = !generationMatches && (lease !== null || run.worker_generation != null);
2127
+ outcome = executionLeaseChanged
2128
+ ? { outcome: "execution_lease_changed" }
2129
+ : await attemptStaleRunRecovery(tx, runId);
2130
2130
  await appendTerminalRunEventInTransaction(tx, runId, STALE_RUN_ERROR_EVENT);
2131
2131
  if (outcome.outcome !== "execution_lease_changed") {
2132
2132
  const projection = await tx.execute({
@@ -3555,7 +3555,9 @@ async function finalizeStaleRun(runId, source, projectThread = true) {
3555
3555
  return true;
3556
3556
  try {
3557
3557
  const { foldReapedRunIntoThread } = await import("./reaped-run-thread.js");
3558
- await foldReapedRunIntoThread(runId);
3558
+ const disposition = await foldReapedRunIntoThread(runId);
3559
+ if (disposition === "retry")
3560
+ return false;
3559
3561
  await getDbExec().execute({
3560
3562
  sql: `UPDATE agent_runs
3561
3563
  SET thread_projection_pending = FALSE
@@ -0,0 +1,27 @@
1
+ import { buildAssistantMessage } from "./thread-data-builder.js";
2
+ import type { RunEvent } from "./types.js";
3
+ interface BuildJournalAssistantMessageOptions {
4
+ suppressInternalContinuation?: boolean;
5
+ preserveUnknownToolOutcomes?: boolean;
6
+ scope?: {
7
+ type: string;
8
+ id: string;
9
+ } | null;
10
+ runDurationMs?: number;
11
+ }
12
+ export declare class RunTranscriptJournalGapError extends Error {
13
+ readonly runId: string;
14
+ readonly missingSequences: number[];
15
+ constructor(runId: string, missingSequences: number[]);
16
+ }
17
+ /** Builds assistant content from the durable event journal and verifies that
18
+ * every published nonterminal event reached that journal before projection.
19
+ */
20
+ export declare function buildAssistantMessageFromRunJournal(input: {
21
+ threadId: string;
22
+ turnId: string;
23
+ runId: string;
24
+ runEvents: readonly RunEvent[];
25
+ options?: BuildJournalAssistantMessageOptions;
26
+ }): Promise<ReturnType<typeof buildAssistantMessage>>;
27
+ export {};
@@ -0,0 +1,79 @@
1
+ import { CHECKPOINT_TERMINAL_EVENT_SEQ, getCurrentTurnRunEventsForThread, } from "./run-store.js";
2
+ import { buildAssistantMessage, foldAssistantTurn, } from "./thread-data-builder.js";
3
+ export class RunTranscriptJournalGapError extends Error {
4
+ runId;
5
+ missingSequences;
6
+ constructor(runId, missingSequences) {
7
+ super(`Run transcript journal is missing events for ${runId}: ${missingSequences.join(", ")}`);
8
+ this.runId = runId;
9
+ this.missingSequences = missingSequences;
10
+ this.name = "RunTranscriptJournalGapError";
11
+ }
12
+ }
13
+ function isTerminalProjectionEvent(event) {
14
+ return (event.type === "done" ||
15
+ event.type === "error" ||
16
+ event.type === "missing_api_key" ||
17
+ event.type === "loop_limit" ||
18
+ event.type === "auto_continue");
19
+ }
20
+ /** Builds assistant content from the durable event journal and verifies that
21
+ * every published nonterminal event reached that journal before projection.
22
+ */
23
+ export async function buildAssistantMessageFromRunJournal(input) {
24
+ const turnJournal = await getCurrentTurnRunEventsForThread(input.threadId, input.turnId);
25
+ const currentRunPersisted = turnJournal.filter((event) => event.runId === input.runId && event.seq < CHECKPOINT_TERMINAL_EVENT_SEQ);
26
+ const persistedSequences = new Set(currentRunPersisted.map(({ seq }) => seq));
27
+ const missingSequences = input.runEvents
28
+ .filter(({ seq, event }) => !isTerminalProjectionEvent(event) && !persistedSequences.has(seq))
29
+ .map(({ seq }) => seq);
30
+ if (missingSequences.length > 0) {
31
+ throw new RunTranscriptJournalGapError(input.runId, missingSequences);
32
+ }
33
+ const currentRunEndIndex = turnJournal.reduce((lastIndex, { runId }, index) => runId === input.runId ? index : lastIndex, -1);
34
+ const relevantJournal = currentRunEndIndex === -1
35
+ ? []
36
+ : turnJournal
37
+ .slice(0, currentRunEndIndex + 1)
38
+ .filter(({ runId, seq }) => runId !== input.runId || seq < CHECKPOINT_TERMINAL_EVENT_SEQ);
39
+ const runGroups = new Map();
40
+ for (const { runId, seq, event } of relevantJournal) {
41
+ const events = runGroups.get(runId) ?? [];
42
+ events.push({ seq, event });
43
+ runGroups.set(runId, events);
44
+ }
45
+ if (!runGroups.has(input.runId))
46
+ runGroups.set(input.runId, []);
47
+ let repo = { messages: [] };
48
+ for (const [runId, persistedRunEvents] of runGroups) {
49
+ const runEvents = persistedRunEvents.sort((left, right) => left.seq - right.seq);
50
+ if (runId === input.runId) {
51
+ for (const event of input.runEvents) {
52
+ if (isTerminalProjectionEvent(event.event) &&
53
+ !persistedSequences.has(event.seq)) {
54
+ runEvents.push(event);
55
+ }
56
+ }
57
+ runEvents.sort((left, right) => left.seq - right.seq);
58
+ }
59
+ const assistant = buildAssistantMessage(runEvents, runId, runId === input.runId
60
+ ? { ...input.options, turnId: input.turnId }
61
+ : {
62
+ suppressInternalContinuation: input.options?.suppressInternalContinuation,
63
+ preserveUnknownToolOutcomes: input.options?.preserveUnknownToolOutcomes,
64
+ scope: input.options?.scope,
65
+ turnId: input.turnId,
66
+ });
67
+ if (!assistant)
68
+ continue;
69
+ repo = foldAssistantTurn(repo, assistant, {
70
+ runId,
71
+ turnId: input.turnId,
72
+ });
73
+ }
74
+ const entry = repo.messages.at(-1);
75
+ const message = entry && typeof entry === "object" && "message" in entry
76
+ ? entry.message
77
+ : entry;
78
+ return (message ?? null);
79
+ }
@@ -3415,7 +3415,7 @@ function assistantContentWeight(content) {
3415
3415
  }
3416
3416
  return weight;
3417
3417
  }
3418
- function appendFoldedContent(existing, incoming) {
3418
+ function appendFoldedContent(existing, incoming, separateRunBoundary = false) {
3419
3419
  const merged = existing.map((p) => ({ ...p }));
3420
3420
  for (const part of incoming) {
3421
3421
  if (part?.type === "tool-call" && part.result !== undefined) {
@@ -3441,11 +3441,12 @@ function appendFoldedContent(existing, incoming) {
3441
3441
  typeof part.text === "string" &&
3442
3442
  last?.type === "text" &&
3443
3443
  typeof last.text === "string") {
3444
- last.text = `${last.text}${part.text}`;
3444
+ last.text = `${last.text}${separateRunBoundary ? "\n\n" : ""}${part.text}`;
3445
3445
  }
3446
3446
  else {
3447
3447
  merged.push({ ...part });
3448
3448
  }
3449
+ separateRunBoundary = false;
3449
3450
  }
3450
3451
  return normalizeAssistantToolCallIds({
3451
3452
  role: "assistant",
@@ -3476,15 +3477,34 @@ export function foldAssistantTurn(repo, assistantMsg, options) {
3476
3477
  ? assistantMsg.content
3477
3478
  : [];
3478
3479
  const existingFolded = foldedRunIdsOf(lastMsg);
3480
+ const incomingFolded = foldedRunIdsOf(assistantMsg);
3479
3481
  const runAlreadyFolded = !!runId &&
3480
3482
  (existingFolded.includes(runId) || getMessageRunId(lastMsg) === runId);
3481
- const mergedContent = runAlreadyFolded
3482
- ? assistantContentWeight(incomingContent) >
3483
- assistantContentWeight(existingContent)
3484
- ? incomingContent
3485
- : existingContent
3486
- : appendFoldedContent(existingContent, incomingContent);
3487
- const mergedFolded = Array.from(new Set([...existingFolded, ...(runId ? [runId] : [])]));
3483
+ const supersededRunReplay = runAlreadyFolded && !!runId && getMessageRunId(lastMsg) !== runId;
3484
+ const incomingCoversExistingRuns = existingFolded.length > 0 &&
3485
+ !!runId &&
3486
+ incomingFolded.includes(runId) &&
3487
+ existingFolded.every((foldedRunId) => incomingFolded.includes(foldedRunId));
3488
+ let mergedContent = existingContent;
3489
+ if (!supersededRunReplay) {
3490
+ if (incomingCoversExistingRuns) {
3491
+ mergedContent = incomingContent;
3492
+ }
3493
+ else if (runAlreadyFolded) {
3494
+ if (assistantContentWeight(incomingContent) >
3495
+ assistantContentWeight(existingContent)) {
3496
+ mergedContent = incomingContent;
3497
+ }
3498
+ }
3499
+ else {
3500
+ mergedContent = appendFoldedContent(existingContent, incomingContent, lastMsg.status?.type === "incomplete");
3501
+ }
3502
+ }
3503
+ const mergedFolded = Array.from(new Set([
3504
+ ...existingFolded,
3505
+ ...(supersededRunReplay ? [] : incomingFolded),
3506
+ ...(runId ? [runId] : []),
3507
+ ]));
3488
3508
  const existingCustom = lastMsg.metadata?.custom && typeof lastMsg.metadata.custom === "object"
3489
3509
  ? lastMsg.metadata.custom
3490
3510
  : {};
@@ -3494,7 +3514,7 @@ export function foldAssistantTurn(repo, assistantMsg, options) {
3494
3514
  : {};
3495
3515
  const mergedCustom = {
3496
3516
  ...existingCustom,
3497
- ...incomingCustom,
3517
+ ...(supersededRunReplay ? {} : incomingCustom),
3498
3518
  turnId,
3499
3519
  foldedRunIds: mergedFolded,
3500
3520
  };
@@ -3512,8 +3532,9 @@ export function foldAssistantTurn(repo, assistantMsg, options) {
3512
3532
  if (mergedDurationMs != null) {
3513
3533
  mergedCustom[ASSISTANT_RUN_DURATION_METADATA_KEY] = mergedDurationMs;
3514
3534
  }
3515
- if (incomingCustom.continued !== true)
3535
+ if (!supersededRunReplay && incomingCustom.continued !== true) {
3516
3536
  delete mergedCustom.continued;
3537
+ }
3517
3538
  // A turn's failure is its newest run's: a run that continued past an
3518
3539
  // earlier stop clears the stop's error instead of inheriting it.
3519
3540
  if (!runAlreadyFolded && incomingCustom.runError === undefined) {
@@ -3525,10 +3546,14 @@ export function foldAssistantTurn(repo, assistantMsg, options) {
3525
3546
  role: "assistant",
3526
3547
  content: mergedContent,
3527
3548
  }).content,
3528
- status: assistantMsg.status ?? lastMsg.status,
3549
+ status: supersededRunReplay
3550
+ ? lastMsg.status
3551
+ : (assistantMsg.status ?? lastMsg.status),
3529
3552
  metadata: {
3530
3553
  ...lastMsg.metadata,
3531
- runId: runId ?? lastMsg.metadata?.runId,
3554
+ runId: supersededRunReplay
3555
+ ? lastMsg.metadata?.runId
3556
+ : (runId ?? lastMsg.metadata?.runId),
3532
3557
  custom: mergedCustom,
3533
3558
  },
3534
3559
  };
@@ -139,8 +139,8 @@ export declare function createAutomationCallbackHandler(runtime: AutomationRunti
139
139
  duplicate?: undefined;
140
140
  eventId?: undefined;
141
141
  } | {
142
- error?: undefined;
143
142
  accepted: boolean;
144
143
  duplicate: boolean;
145
144
  eventId: string;
145
+ error?: undefined;
146
146
  }>;
@@ -183,5 +183,5 @@ declare function replacePlaceholders(dir: string, appName: string, appTitle: str
183
183
  declare function copyDir(src: string, dest: string, root?: string, opts?: {
184
184
  skipExisting?: boolean;
185
185
  materializeSymlinks?: boolean;
186
- }): void;
186
+ }, destinationRoot?: string): void;
187
187
  declare function shouldSkipScaffoldEntry(name: string, srcPath?: string): boolean;
@@ -5,7 +5,8 @@ import path from "path";
5
5
  import { fileURLToPath, pathToFileURL } from "url";
6
6
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
7
7
  import { DISPATCH_WORKSPACE_ROOT_REDIRECTS, getWorkspaceAppIdValidationError, } from "../shared/workspace-app-id.js";
8
- import { setupAgentSymlinks } from "./setup-agents.js";
8
+ import { createAgentSymlink, prepareAgentSymlink, rebaseAgentSymlinkTarget, setupAgentSymlinks, } from "./setup-agents.js";
9
+ import { resolveTarExecutable } from "./tar-command.js";
9
10
  import { applyTemplateLayer, readTemplateLayer } from "./template-layer.js";
10
11
  import { coreTemplates, getTemplate, allTemplateNames, } from "./templates-meta.js";
11
12
  import { addConfiguredMigrationDependencies, readUpgradeEnvironment, } from "./upgrade.js";
@@ -674,22 +675,11 @@ function linkWorkspaceRootSkills(targetDir) {
674
675
  const linkPath = path.join(agentsDir, "skills");
675
676
  const target = "../packages/shared/.agents/skills";
676
677
  fs.mkdirSync(agentsDir, { recursive: true });
678
+ const state = prepareAgentSymlink(linkPath, target, "dir");
679
+ if (state !== "create")
680
+ return;
677
681
  try {
678
- const stat = fs.lstatSync(linkPath);
679
- if (stat.isSymbolicLink()) {
680
- if (fs.readlinkSync(linkPath) === target)
681
- return;
682
- fs.unlinkSync(linkPath);
683
- }
684
- else {
685
- return;
686
- }
687
- }
688
- catch {
689
- // Missing link; create below.
690
- }
691
- try {
692
- fs.symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
682
+ createAgentSymlink(target, linkPath, "dir");
693
683
  }
694
684
  catch {
695
685
  try {
@@ -1338,7 +1328,7 @@ function localPackageTarball(packageDir) {
1338
1328
  }
1339
1329
  const rawTarball = path.join(packDir, tarballs[0]);
1340
1330
  const unpackDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-native-local-package-unpack-"));
1341
- execFileSync("tar", ["-xzf", rawTarball, "-C", unpackDir], {
1331
+ execFileSync(resolveTarExecutable(), ["-xzf", rawTarball, "-C", unpackDir], {
1342
1332
  stdio: "pipe",
1343
1333
  });
1344
1334
  rewritePublishedPackageManifest(path.join(unpackDir, "package", "package.json"));
@@ -1948,7 +1938,7 @@ function tarExtractArgs(tarPath, destDir, options = {}) {
1948
1938
  ];
1949
1939
  }
1950
1940
  function archiveSymlinksForExtraction(tarPath) {
1951
- const listing = execFileSync("tar", ["tvzf", tarPath], {
1941
+ const listing = execFileSync(resolveTarExecutable(), ["tvzf", tarPath], {
1952
1942
  encoding: "utf-8",
1953
1943
  stdio: ["ignore", "pipe", "pipe"],
1954
1944
  maxBuffer: TAR_LISTING_MAX_BUFFER,
@@ -2037,7 +2027,7 @@ function extractTarball(tarPath, destDir, options = {}) {
2037
2027
  const symlinks = options.skipAgentSymlinks || options.untrustedCommunityArchive
2038
2028
  ? archiveSymlinksForExtraction(tarPath)
2039
2029
  : [];
2040
- execFileSync("tar", tarExtractArgs(tarPath, destDir, {
2030
+ execFileSync(resolveTarExecutable(), tarExtractArgs(tarPath, destDir, {
2041
2031
  ...options,
2042
2032
  additionalExcludes: symlinks.map((link) => link.archivePath),
2043
2033
  }), {
@@ -2071,7 +2061,7 @@ async function downloadAndExtract(url, destDir, options = {}) {
2071
2061
  }
2072
2062
  }
2073
2063
  function validateCommunityArchive(tarPath) {
2074
- const listing = execFileSync("tar", ["tvzf", tarPath], {
2064
+ const listing = execFileSync(resolveTarExecutable(), ["tvzf", tarPath], {
2075
2065
  encoding: "utf-8",
2076
2066
  stdio: ["ignore", "pipe", "pipe"],
2077
2067
  maxBuffer: TAR_LISTING_MAX_BUFFER,
@@ -3451,8 +3441,9 @@ function replacePlaceholders(dir, appName, appTitle, workspaceName) {
3451
3441
  fs.writeFileSync(p, next);
3452
3442
  }
3453
3443
  }
3454
- function copyDir(src, dest, root, opts) {
3444
+ function copyDir(src, dest, root, opts, destinationRoot) {
3455
3445
  const resolvedRoot = root ?? path.resolve(src);
3446
+ const resolvedDestinationRoot = destinationRoot ?? path.resolve(dest);
3456
3447
  const skipExisting = opts?.skipExisting ?? false;
3457
3448
  fs.mkdirSync(dest, { recursive: true });
3458
3449
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
@@ -3465,16 +3456,28 @@ function copyDir(src, dest, root, opts) {
3465
3456
  }
3466
3457
  if (entry.isSymbolicLink()) {
3467
3458
  const target = fs.readlinkSync(srcPath);
3468
- const resolvedTarget = path.resolve(path.dirname(srcPath), target);
3469
- if (!opts?.materializeSymlinks &&
3470
- isPathWithin(resolvedRoot, resolvedTarget)) {
3471
- fs.symlinkSync(target, destPath);
3459
+ const copiedTarget = rebaseAgentSymlinkTarget(resolvedRoot, resolvedDestinationRoot, srcPath, destPath, target);
3460
+ if (!opts?.materializeSymlinks && copiedTarget !== undefined) {
3461
+ let stat;
3462
+ try {
3463
+ stat = fs.statSync(srcPath);
3464
+ }
3465
+ catch (error) {
3466
+ if (error.code !== "ENOENT")
3467
+ throw error;
3468
+ if (process.platform === "win32") {
3469
+ throw new Error(`Cannot copy dangling symlink at "${srcPath}" on Windows because its target type is unknown.`);
3470
+ }
3471
+ fs.symlinkSync(copiedTarget, destPath);
3472
+ continue;
3473
+ }
3474
+ createAgentSymlink(copiedTarget, destPath, stat.isDirectory() ? "dir" : "file");
3472
3475
  }
3473
3476
  else {
3474
3477
  try {
3475
3478
  const stat = fs.statSync(srcPath);
3476
3479
  if (stat.isDirectory()) {
3477
- copyDir(srcPath, destPath, resolvedRoot, opts);
3480
+ copyDir(srcPath, destPath, resolvedRoot, opts, resolvedDestinationRoot);
3478
3481
  }
3479
3482
  else {
3480
3483
  fs.copyFileSync(srcPath, destPath);
@@ -3486,7 +3489,7 @@ function copyDir(src, dest, root, opts) {
3486
3489
  }
3487
3490
  }
3488
3491
  else if (entry.isDirectory()) {
3489
- copyDir(srcPath, destPath, resolvedRoot, opts);
3492
+ copyDir(srcPath, destPath, resolvedRoot, opts, resolvedDestinationRoot);
3490
3493
  }
3491
3494
  else {
3492
3495
  fs.copyFileSync(srcPath, destPath);
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { assertAgentNativePackageManifest, } from "../package-lifecycle/manifest.js";
6
+ import { resolveTarExecutable } from "./tar-command.js";
6
7
  const defaultIO = {
7
8
  out: (message) => console.log(message),
8
9
  err: (message) => console.error(message),
@@ -96,7 +97,7 @@ export async function loadLifecyclePackage(name, cwd, spawn = spawnSync) {
96
97
  throw new Error(`npm pack returned an invalid archive name for '${name}'`);
97
98
  }
98
99
  const archive = path.join(stage, filename);
99
- const listed = spawn("tar", ["-tzf", archive], {
100
+ const listed = spawn(resolveTarExecutable(), ["-tzf", archive], {
100
101
  cwd,
101
102
  encoding: "utf8",
102
103
  stdio: "pipe",
@@ -114,7 +115,7 @@ export async function loadLifecyclePackage(name, cwd, spawn = spawnSync) {
114
115
  entry.split("/").includes(".."))) {
115
116
  throw new Error(`Package archive for '${name}' contains an unsafe path`);
116
117
  }
117
- const extracted = spawn("tar", [
118
+ const extracted = spawn(resolveTarExecutable(), [
118
119
  "-xzf",
119
120
  archive,
120
121
  "-C",
@@ -1,2 +1,7 @@
1
+ type SymlinkState = "create" | "matches" | "occupied";
2
+ export declare function prepareAgentSymlink(linkPath: string, target: string, type: "file" | "dir"): SymlinkState;
3
+ export declare function createAgentSymlink(target: string, linkPath: string, type: "file" | "dir"): void;
4
+ export declare function rebaseAgentSymlinkTarget(sourceRoot: string, destinationRoot: string, sourceLinkPath: string, destinationLinkPath: string, target: string): string | undefined;
1
5
  export declare function setupAgentSymlinks(targetDir: string): void;
2
6
  export declare function runSetupAgents(): void;
7
+ export {};