@inflexa-ai/harness 0.43.1 → 0.45.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 (119) hide show
  1. package/dist/agents/report-session-agent.d.ts +1 -1
  2. package/dist/app/approve-plan.d.ts +48 -0
  3. package/dist/app/approve-plan.js +84 -0
  4. package/dist/app/data-profile-view.d.ts +1 -1
  5. package/dist/app/run-activity.js +4 -6
  6. package/dist/app/synthesize-run.js +1 -1
  7. package/dist/contracts/chat-events.d.ts +2 -0
  8. package/dist/contracts/chat-frame.d.ts +7 -0
  9. package/dist/contracts/chat-frame.js +18 -4
  10. package/dist/contracts/chat-parts.d.ts +73 -2
  11. package/dist/contracts/chat-parts.js +3 -1
  12. package/dist/contracts/index.d.ts +3 -1
  13. package/dist/contracts/index.js +1 -0
  14. package/dist/contracts/part-registry.js +3 -0
  15. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  16. package/dist/contracts/schemas/chat-events.js +1 -0
  17. package/dist/contracts/schemas/chat-parts.d.ts +200 -15
  18. package/dist/contracts/schemas/chat-parts.js +63 -12
  19. package/dist/contracts/schemas/index.d.ts +1 -1
  20. package/dist/contracts/schemas/index.js +1 -1
  21. package/dist/contracts/subagent-activity.d.ts +12 -0
  22. package/dist/contracts/subagent-activity.js +76 -0
  23. package/dist/execution/artifact-metadata.js +1 -3
  24. package/dist/execution/post-step-pipeline.js +1 -1
  25. package/dist/execution/run-canceler.js +1 -1
  26. package/dist/execution/run-event-parts.d.ts +6 -5
  27. package/dist/execution/run-event-parts.js +6 -5
  28. package/dist/execution/run-event-stream.d.ts +18 -6
  29. package/dist/execution/run-event-stream.js +67 -17
  30. package/dist/execution/run-synthesis.d.ts +21 -0
  31. package/dist/execution/run-synthesis.js +77 -3
  32. package/dist/index.d.ts +24 -10
  33. package/dist/index.js +17 -3
  34. package/dist/lib/hooks.d.ts +1 -1
  35. package/dist/lib/metrics.d.ts +2 -2
  36. package/dist/loop/run-agent.js +1 -0
  37. package/dist/loop/types.d.ts +2 -0
  38. package/dist/memory/card-builders.d.ts +11 -0
  39. package/dist/memory/card-builders.js +35 -21
  40. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  41. package/dist/memory/conversation-display-recorder.js +10 -1
  42. package/dist/memory/conversation-display-storage.d.ts +9 -2
  43. package/dist/memory/conversation-display-storage.js +18 -3
  44. package/dist/memory/thread-history.d.ts +8 -1
  45. package/dist/memory/thread-history.js +13 -3
  46. package/dist/memory/thread-store.d.ts +13 -0
  47. package/dist/memory/thread-store.js +15 -1
  48. package/dist/prompts/conversation.d.ts +1 -1
  49. package/dist/prompts/conversation.js +3 -1
  50. package/dist/provenance/seam.d.ts +16 -5
  51. package/dist/providers/ai-sdk.js +6 -4
  52. package/dist/reference-data/receipt.d.ts +16 -4
  53. package/dist/reference-data/receipt.js +20 -3
  54. package/dist/report-model/pin-snapshot.d.ts +1 -1
  55. package/dist/report-model/pin-snapshot.js +4 -52
  56. package/dist/runtime/assemble.d.ts +9 -2
  57. package/dist/runtime/assemble.js +13 -2
  58. package/dist/runtime/boot.d.ts +3 -0
  59. package/dist/runtime/boot.js +10 -1
  60. package/dist/sandbox/client.d.ts +1 -1
  61. package/dist/sandbox/create-sandbox.js +1 -1
  62. package/dist/sandbox/docker-client.js +1 -1
  63. package/dist/sandbox/k8s-client.js +5 -5
  64. package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
  65. package/dist/sandbox/sandbox-step-translate.js +24 -0
  66. package/dist/schemas/artifact-manifest.d.ts +1 -1
  67. package/dist/schemas/artifact-manifest.js +11 -9
  68. package/dist/schemas/plan-schemas.d.ts +18 -18
  69. package/dist/schemas/run-synthesis.d.ts +2 -2
  70. package/dist/state/analyses-metrics.d.ts +79 -7
  71. package/dist/state/analyses-metrics.js +141 -6
  72. package/dist/state/artifacts.d.ts +50 -3
  73. package/dist/state/artifacts.js +67 -7
  74. package/dist/state/data-profile.d.ts +4 -2
  75. package/dist/state/data-profile.js +7 -5
  76. package/dist/state/index.d.ts +9 -8
  77. package/dist/state/index.js +5 -5
  78. package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
  79. package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
  80. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
  81. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
  82. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
  83. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
  84. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
  85. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
  86. package/dist/state/migrations/index.js +8 -0
  87. package/dist/state/plans.d.ts +21 -0
  88. package/dist/state/plans.js +25 -0
  89. package/dist/state/runs.d.ts +36 -5
  90. package/dist/state/runs.js +65 -19
  91. package/dist/state/schema.d.ts +2 -0
  92. package/dist/tasks/data-profile-activity.d.ts +7 -1
  93. package/dist/tasks/data-profile-activity.js +18 -9
  94. package/dist/tasks/data-profile.d.ts +15 -1
  95. package/dist/tasks/data-profile.js +18 -13
  96. package/dist/tools/approval/gateway.js +28 -5
  97. package/dist/tools/display/show-user.d.ts +1 -1
  98. package/dist/tools/execute-analysis.d.ts +51 -0
  99. package/dist/tools/execute-analysis.js +241 -109
  100. package/dist/tools/report-session/derive-table.js +1 -1
  101. package/dist/tools/report-session/examine-page.js +1 -1
  102. package/dist/tools/report-session/list-artifacts.js +2 -2
  103. package/dist/tools/report-session/preview-report.d.ts +1 -1
  104. package/dist/tools/report-session/preview-report.js +6 -6
  105. package/dist/tools/report-session/record-version.d.ts +1 -1
  106. package/dist/tools/report-session/record-version.js +4 -4
  107. package/dist/tools/research/inspect-data-profile.d.ts +1 -1
  108. package/dist/tools/research/inspect-run.d.ts +1 -1
  109. package/dist/tools/research/inspect-run.js +7 -7
  110. package/dist/tools/workspace/mutator.js +2 -2
  111. package/dist/workflows/execute-analysis.d.ts +4 -3
  112. package/dist/workflows/execute-analysis.js +38 -25
  113. package/dist/workflows/plan/graph.d.ts +16 -0
  114. package/dist/workflows/plan/graph.js +36 -0
  115. package/dist/workflows/sandbox-step.d.ts +2 -2
  116. package/dist/workflows/sandbox-step.js +16 -14
  117. package/dist/workspace/filesystem.js +18 -22
  118. package/dist/workspace/paths.d.ts +7 -4
  119. package/package.json +4 -4
@@ -50,8 +50,9 @@
50
50
  * the run before any step starts
51
51
  * - a `suspend` error of a synthesis model request suspends the run in
52
52
  * place of the synthesis failure
53
- * - the analysis flips to `suspended_insufficient_funds` in
54
- * `collectAndComplete`. The paused parent lands in `CANCELLED`, a
53
+ * - the run row and the analysis flip to `suspended_insufficient_funds` in
54
+ * `collectAndComplete`. The run status is terminal and does not block a
55
+ * new run of the plan. The paused parent lands in `CANCELLED`, a
55
56
  * durably reschedulable state; re-driving it is a future enhancement,
56
57
  * not yet wired here. The reason of the host rides to the run row, the
57
58
  * charge close, the metric, and the terminal part, unread.
@@ -160,7 +161,7 @@ export interface ExecuteAnalysisInput {
160
161
  * path returns — the caller joined an in-flight run, no new workflow was
161
162
  * started. Every other variant is a terminal status the parent reached.
162
163
  */
163
- export type ExecuteAnalysisFinalStatus = "running" | "completed" | "partial" | "failed" | "canceled";
164
+ export type ExecuteAnalysisFinalStatus = "running" | "completed" | "partial" | "failed" | "canceled" | "suspended_insufficient_funds";
164
165
  export interface ExecuteAnalysisResult {
165
166
  readonly runId: string;
166
167
  readonly workflowId: string;
@@ -50,8 +50,9 @@
50
50
  * the run before any step starts
51
51
  * - a `suspend` error of a synthesis model request suspends the run in
52
52
  * place of the synthesis failure
53
- * - the analysis flips to `suspended_insufficient_funds` in
54
- * `collectAndComplete`. The paused parent lands in `CANCELLED`, a
53
+ * - the run row and the analysis flip to `suspended_insufficient_funds` in
54
+ * `collectAndComplete`. The run status is terminal and does not block a
55
+ * new run of the plan. The paused parent lands in `CANCELLED`, a
55
56
  * durably reschedulable state; re-driving it is a future enhancement,
56
57
  * not yet wired here. The reason of the host rides to the run row, the
57
58
  * charge close, the metric, and the terminal part, unread.
@@ -79,12 +80,13 @@ import { ATTR_INFLEXA_STEP_ID, stableSpan } from "../lib/otel-spans.js";
79
80
  import { recordRunCompleted, recordStepCompleted, stepOutcomeOf } from "../lib/metrics.js";
80
81
  import { describeDbError } from "../lib/db-result.js";
81
82
  import { unwrapOrThrow } from "../lib/result.js";
82
- import { countArtifactsForRun, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, queryStepsByRun, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
83
+ import { countArtifactsByRuns, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, queryStepsByRun, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
83
84
  import { addChatUsage, hasReportedUsage } from "../loop/metrics.js";
85
+ import { flattenDataPart } from "../contracts/chat-frame.js";
84
86
  import { SYNTHESIS_AGENT_ID, loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
85
87
  import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing, emptyStepMemory } from "../prompts/briefing.js";
86
88
  import { SYNTHESIS_STEP_ID, runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
87
- import { isChatDataPart } from "../sandbox/sandbox-step-translate.js";
89
+ import { isChatDataPart, synthesisEmission } from "../sandbox/sandbox-step-translate.js";
88
90
  import { synthesizeRun } from "../app/synthesize-run.js";
89
91
  import { computeTopologicalLevels, scheduleReady, validatePlanDag } from "./execute-analysis-scheduler.js";
90
92
  import { recordCancelledChild } from "./metrics.js";
@@ -250,7 +252,7 @@ export async function gatherStepPackage(args) {
250
252
  throw new Error(`executeAnalysis: step "${stepId}" missing from planStepById — every step must carry its plan data`);
251
253
  }
252
254
  const analysisId = input.analysisId;
253
- const workspaceRoot = deps.resolveWorkspaceRoot(analysisId);
255
+ const workspaceRoot = await deps.resolveWorkspaceRoot(analysisId);
254
256
  // The paths the agent actually sees: the tree is bind-mounted at
255
257
  // `/{analysisId}`, and the step's writable directory is its cwd. Derived
256
258
  // through the same helpers the sandbox mount and the mutate tools use, so
@@ -359,9 +361,20 @@ export function synthesisRowUpdate(outcome, durationMs) {
359
361
  */
360
362
  export function synthesizeFindings(args) {
361
363
  const { analysisId, runId, completedSteps, session, deps } = args;
364
+ const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis.synthesis");
365
+ // Observation is a diagnostic channel: a failed progress write is logged and never fails the synthesis.
366
+ let progressSeq = 0;
367
+ const writeProgressAlone = async (part, phase) => {
368
+ try {
369
+ await DBOS.writeStream("events", part);
370
+ }
371
+ catch (err) {
372
+ logger.warn("synthesis progress write failed (non-fatal)", { phase, ...logger.errorFields(err) });
373
+ }
374
+ };
362
375
  const emit = (event) => {
363
376
  const part = isChatDataPart(event)
364
- ? event
377
+ ? flattenDataPart(event)
365
378
  : {
366
379
  type: "data-loop-event",
367
380
  data: { stepId: "synthesis", event },
@@ -395,14 +408,15 @@ export function synthesizeFindings(args) {
395
408
  // call path.
396
409
  session: forStep(session, SYNTHESIS_STEP_ID),
397
410
  emit,
398
- onProgress: (phase, activity, extra = {}) => DBOS.writeStream("events", {
399
- type: "data-synthesis-progress",
400
- id: `synthesis-progress-${runId}`,
401
- runId,
402
- phase,
403
- activity,
404
- ...extra,
405
- }),
411
+ onProgress: async (phase, activity, extra = {}) => {
412
+ // Synthesis runs inside one `DBOS.runStep`, thus `DBOS.now()` reads the wall clock here, and a rerun of
413
+ // the step after a crash writes its entries again under the same ids. A reader dedups an entry by id.
414
+ const seq = progressSeq++;
415
+ const at = new Date(await DBOS.now()).toISOString();
416
+ const parts = synthesisEmission(runId, SYNTHESIS_STEP_ID, seq, phase, activity, at, extra);
417
+ await writeProgressAlone(parts.progress, phase);
418
+ await writeProgressAlone(parts.entry, phase);
419
+ },
406
420
  });
407
421
  }
408
422
  // ── Workflow registration ────────────────────────────────────────────
@@ -661,10 +675,10 @@ async function validateAndInit(input, runId, deps) {
661
675
  return { kind: "joined-existing", runId: existing.runId };
662
676
  }
663
677
  await DBOS.runStep(async () => {
664
- // A resolver throw here (unknown analysis, unresolvable root) fails the
678
+ // A resolver rejection here (unknown analysis, unresolvable root) fails the
665
679
  // step durably — exactly the contract the workspace-root-resolution spec
666
680
  // requires of failures inside DBOS bodies.
667
- await mkdir(join(deps.resolveWorkspaceRoot(input.analysisId), runDir(runId)), {
681
+ await mkdir(join(await deps.resolveWorkspaceRoot(input.analysisId), runDir(runId)), {
668
682
  recursive: true,
669
683
  });
670
684
  }, { name: "init-run-filesystem" });
@@ -1086,7 +1100,7 @@ async function collectAndComplete(args) {
1086
1100
  durationMs: terminalAtMs - startedAtMs,
1087
1101
  }, input.runSession);
1088
1102
  await ledgerStep(logger.with({ status }), "persist-final-status", async () => {
1089
- const written = await updateRunStatus(deps.pool, runId, status, failureReason ?? (status === "canceled" && suspended === null ? "external_cancel" : null));
1103
+ const written = await updateRunStatus(deps.pool, runId, status, failureReason ?? (status === "canceled" ? "external_cancel" : null));
1090
1104
  if (written.isErr())
1091
1105
  return written;
1092
1106
  // The outcome metrics ride inside the step that persists the terminal status: DBOS
@@ -1119,10 +1133,9 @@ async function collectAndComplete(args) {
1119
1133
  await ledgerStep(logger.with({ synthesisStatus: outcome.status }), "persist-synthesis-outcome", () => setRunSynthesisOutcome(deps.pool, runId, outcome.status, outcome.reason));
1120
1134
  }
1121
1135
  // Sweep never-started steps to `skipped` — but ONLY on genuinely-terminal
1122
- // paths. The gate is the BRANCH, not the written run status: the resumable
1123
- // suspension below also writes "canceled", yet its pending rows must survive
1124
- // for the resumed workflow to execute. Log-don't-rollback, like every other
1125
- // finalisation step here.
1136
+ // paths. The gate is the BRANCH, not the written run status: the pending rows
1137
+ // of a suspended run must survive for a later resume. Log-don't-rollback, like
1138
+ // every other finalisation step here.
1126
1139
  if (suspended === null) {
1127
1140
  await ledgerStep(logger, "sweep-pending-steps", () => sweepPendingStepExecutions(deps.pool, runId));
1128
1141
  }
@@ -1149,9 +1162,9 @@ async function collectAndComplete(args) {
1149
1162
  // beat the CLI's poll-and-shutdown). These branches only fan out the UI stream part, whose
1150
1163
  // completed/failed shapes genuinely differ — the provenance record does not.
1151
1164
  if (status === "completed" || status === "partial") {
1152
- const artifactCount = await DBOS.runStep(async () => await countArtifactsForRun(deps.pool, input.analysisId, runId).unwrapOr(0), {
1153
- name: "count-run-artifacts",
1154
- });
1165
+ const artifactCount = await DBOS.runStep(async () => await countArtifactsByRuns(deps.pool, input.analysisId, [runId])
1166
+ .map((counts) => counts[0]?.count ?? 0)
1167
+ .unwrapOr(0), { name: "count-run-artifacts" });
1155
1168
  await emitStreamPart({
1156
1169
  type: "data-run-completed",
1157
1170
  runId,
@@ -1197,7 +1210,7 @@ async function collectAndComplete(args) {
1197
1210
  function deriveFinalStatus(args) {
1198
1211
  const { totalSteps, completed, failed, canceled, suspended } = args;
1199
1212
  if (suspended)
1200
- return "canceled";
1213
+ return "suspended_insufficient_funds";
1201
1214
  if (failed.size > 0) {
1202
1215
  return completed.size > 0 ? "partial" : "failed";
1203
1216
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The graph of a stored plan: each step with its agent, its dependencies, and its
3
+ * level. The levels and the agent fallback are those of `data-dag-state`, so a
4
+ * host draws the same graph before, during, and after a run.
5
+ */
6
+ import type { PlanGraphStep } from "../../contracts/chat-parts.js";
7
+ /**
8
+ * Give the graph of a stored plan document, in the order of the plan. An empty
9
+ * step list gives an empty graph. A document that fails the stored plan schema, or
10
+ * whose steps have a duplicate id, a missing dependency, or a cycle, gives `null`
11
+ * (a plan that no run can execute has no levels).
12
+ *
13
+ * The throw of `validatePlanDag` is caught here, at the boundary: the function is a
14
+ * read for a caller that has no use for the typed error.
15
+ */
16
+ export declare function planGraphOf(plan: unknown): PlanGraphStep[] | null;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The graph of a stored plan: each step with its agent, its dependencies, and its
3
+ * level. The levels and the agent fallback are those of `data-dag-state`, so a
4
+ * host draws the same graph before, during, and after a run.
5
+ */
6
+ import { AnalysisPlanSchema } from "../../schemas/workflow-state.js";
7
+ import { computeTopologicalLevels, validatePlanDag } from "../execute-analysis-scheduler.js";
8
+ /**
9
+ * Give the graph of a stored plan document, in the order of the plan. An empty
10
+ * step list gives an empty graph. A document that fails the stored plan schema, or
11
+ * whose steps have a duplicate id, a missing dependency, or a cycle, gives `null`
12
+ * (a plan that no run can execute has no levels).
13
+ *
14
+ * The throw of `validatePlanDag` is caught here, at the boundary: the function is a
15
+ * read for a caller that has no use for the typed error.
16
+ */
17
+ export function planGraphOf(plan) {
18
+ const parsed = AnalysisPlanSchema.safeParse(plan);
19
+ if (!parsed.success)
20
+ return null;
21
+ const steps = parsed.data.steps;
22
+ try {
23
+ validatePlanDag(steps);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ const levels = computeTopologicalLevels(steps);
29
+ return steps.map((step) => ({
30
+ id: step.id,
31
+ name: step.name,
32
+ agent: step.agent ?? "unknown",
33
+ dependsOn: [...step.depends_on],
34
+ level: levels.get(step.id) ?? 0,
35
+ }));
36
+ }
@@ -263,12 +263,12 @@ export interface SandboxStepDeps {
263
263
  * agent from the catalog, wires its workspace tools, and returns the
264
264
  * configured `AgentDefinition` runAgent will drive.
265
265
  */
266
- buildAgent(ctx: SandboxAgentBuildContext): AgentDefinition;
266
+ buildAgent(ctx: SandboxAgentBuildContext): Promise<AgentDefinition>;
267
267
  /**
268
268
  * Resolve the absolute writable artifact prefix for this step. The
269
269
  * workspace mutate tools enforce write confinement against this path.
270
270
  */
271
- resolveWritePrefix(input: SandboxStepInput): string;
271
+ resolveWritePrefix(input: SandboxStepInput): Promise<string>;
272
272
  }
273
273
  /**
274
274
  * Per-step context passed to every post-step dep. Carries the durable input
@@ -38,7 +38,7 @@ import { durableStep } from "../loop/run-step.js";
38
38
  import { maskExcept } from "../loop/tool-mask.js";
39
39
  import { READ_TOOL_OUTPUT_TOOL_ID } from "../loop/tool-output.js";
40
40
  import { lastExecOutcome } from "../sandbox/exec-outcome.js";
41
- import { activityForTool, applyTreeDelta, isChatDataPart, sandboxTreeDelta, stepPartId } from "../sandbox/sandbox-step-translate.js";
41
+ import { activityEmission, activityForTool, applyTreeDelta, isChatDataPart, sandboxTreeDelta, stepPartId } from "../sandbox/sandbox-step-translate.js";
42
42
  import { createDetailResolver } from "../tools/detail-resolver.js";
43
43
  import { forSubAgent } from "../auth/types.js";
44
44
  import { walkStepArtifacts } from "../execution/post-step.js";
@@ -176,7 +176,7 @@ export async function runSandboxStepBody(input, deps) {
176
176
  // "conversation-agent"). `run-agent.ts` builds source from
177
177
  // `session.provenance.agentId`, and billing tags ride the same provenance.
178
178
  const session = forSubAgent(input.runSession, input.agentId);
179
- const writePrefix = deps.resolveWritePrefix(input);
179
+ const writePrefix = await deps.resolveWritePrefix(input);
180
180
  const lineageCollector = createLineageCollector(input);
181
181
  // (2a) sandbox.mint — checkpoint the machine's identity (its name)
182
182
  // BEFORE it is spawned (see the harness-sandbox-exec spec). Durable-before-create is what makes (2b)
@@ -220,7 +220,7 @@ export async function runSandboxStepBody(input, deps) {
220
220
  const stepDeadlineMs = (await DBOS.now()) + (input.timeoutSeconds ?? DEFAULT_STEP_TIMEOUT_SECONDS) * 1000;
221
221
  const blockerHolder = createBlockerHolder();
222
222
  const fileMetadata = createFileMetadataCell();
223
- const agent = deps.buildAgent({
223
+ const agent = await deps.buildAgent({
224
224
  input,
225
225
  session,
226
226
  sandbox,
@@ -266,17 +266,19 @@ export async function runSandboxStepBody(input, deps) {
266
266
  logger.warn("emitToParentStream failed (non-fatal)", logger.errorFields(err));
267
267
  }
268
268
  };
269
- // Schema-conformant step-activity emitter — stable id per (runId, stepId)
270
- // so the run-stream fold collapses every phase transition latest-wins.
271
- const activityId = stepPartId("step-activity", input.runId, input.stepId);
272
- const emitActivity = (phase, activity) => safeEmit({
273
- type: "data-step-activity",
274
- id: activityId,
275
- runId: input.runId,
276
- stepId: input.stepId,
277
- phase,
278
- activity,
279
- });
269
+ // Schema-conformant step-activity emitter. `data-step-activity` has a stable id per (runId, stepId),
270
+ // so the run-stream fold collapses every phase transition latest-wins. Each emission also leaves one
271
+ // `data-activity-entry` that the fold keeps, thus a late subscriber gets each line with its time. The
272
+ // two parts share one checkpointed clock read. `activitySeq` advances before the clock read and the
273
+ // writes, and each write fails alone through `safeEmit`.
274
+ let activitySeq = 0;
275
+ const emitActivity = async (phase, activity) => {
276
+ const seq = activitySeq++;
277
+ const at = new Date(await DBOS.now()).toISOString();
278
+ const parts = activityEmission(input, seq, phase, activity, at);
279
+ await safeEmit(parts.activity);
280
+ await safeEmit(parts.entry);
281
+ };
280
282
  // Single exit for the two terminal failure paths (agent loop, lineage
281
283
  // attestation). Tears the sandbox down, logs the internal detail for
282
284
  // operators, then persists + emits only a generic phrase and re-raises a
@@ -32,7 +32,7 @@ import { lstat as fsLstat, open as fsOpen, readdir as fsReaddir, stat as fsStat
32
32
  import { constants as fsConstants } from "node:fs";
33
33
  import { createInterface } from "node:readline";
34
34
  import { join } from "node:path";
35
- import { ResultAsync, err, errAsync, ok } from "neverthrow";
35
+ import { ResultAsync, ok } from "neverthrow";
36
36
  import { scopeResource } from "../auth/types.js";
37
37
  import { tryFetch, tryFs } from "../lib/fs-result.js";
38
38
  import { classifyWithinRoot } from "../lib/fs-helpers.js";
@@ -41,7 +41,7 @@ import { resolveWorkspacePath } from "./paths.js";
41
41
  export function createWorkspaceFilesystem(deps) {
42
42
  const { resolveWorkspaceRoot, presignedFallback } = deps;
43
43
  /**
44
- * `ResolveWorkspaceRoot` signals an unresolvable resource by throwing — the contract that
44
+ * `ResolveWorkspaceRoot` signals an unresolvable resource by a rejection — the contract that
45
45
  * makes a resolution failure inside a DBOS body a durably-failed step. This seam is not a
46
46
  * DBOS body: it promises `Result`, and the read tools it backs are reachable from a live
47
47
  * chat turn whose analysis folder may have been moved or deleted since the turn began.
@@ -50,14 +50,12 @@ export function createWorkspaceFilesystem(deps) {
50
50
  */
51
51
  function resolveFor(session, path, workingDir) {
52
52
  const { resourceId: analysisId } = scopeResource(session.scope);
53
- let workspaceRoot;
54
- try {
55
- workspaceRoot = resolveWorkspaceRoot(analysisId);
56
- }
57
- catch (cause) {
58
- return err({ type: "read_failed", op: "workspace.resolveWorkspaceRoot", path: analysisId, cause });
59
- }
60
- return ok({ resolved: resolveWorkspacePath({ workspaceRoot, analysisId, path, workingDir }), workspaceRoot });
53
+ return ResultAsync.fromThrowable(resolveWorkspaceRoot, (cause) => ({
54
+ type: "read_failed",
55
+ op: "workspace.resolveWorkspaceRoot",
56
+ path: analysisId,
57
+ cause,
58
+ }))(analysisId).map((workspaceRoot) => ({ resolved: resolveWorkspacePath({ workspaceRoot, analysisId, path, workingDir }), workspaceRoot }));
61
59
  }
62
60
  /**
63
61
  * Lexical resolution PLUS a symlink-following confinement check — the single
@@ -70,18 +68,16 @@ export function createWorkspaceFilesystem(deps) {
70
68
  * presigned-fallback handling still runs (a non-existent path leaks nothing).
71
69
  */
72
70
  function confinedResolve(session, path, workingDir) {
73
- const base = resolveFor(session, path, workingDir);
74
- if (base.isErr())
75
- return errAsync(base.error);
76
- const { resolved, workspaceRoot } = base.value;
77
- if (resolved.kind === "out_of_scope")
78
- return okAsync(resolved);
79
- return ResultAsync.fromPromise(classifyWithinRoot(workspaceRoot, resolved.absolute), (cause) => ({
80
- type: "read_failed",
81
- op: "workspace.confineRealpath",
82
- path: resolved.absolute,
83
- cause,
84
- })).map((verdict) => (verdict === "escaped" ? { kind: "out_of_scope" } : resolved));
71
+ return resolveFor(session, path, workingDir).andThen(({ resolved, workspaceRoot }) => {
72
+ if (resolved.kind === "out_of_scope")
73
+ return okAsync(resolved);
74
+ return ResultAsync.fromPromise(classifyWithinRoot(workspaceRoot, resolved.absolute), (cause) => ({
75
+ type: "read_failed",
76
+ op: "workspace.confineRealpath",
77
+ path: resolved.absolute,
78
+ cause,
79
+ })).map((verdict) => (verdict === "escaped" ? { kind: "out_of_scope" } : resolved));
80
+ });
85
81
  }
86
82
  return {
87
83
  readFile({ session, path, maxBytes, headLines, tailLines, workingDir }) {
@@ -43,11 +43,14 @@
43
43
  * - **Stable while the resource has an active run** — derived paths are
44
44
  * recorded in durable step outputs; preventing mid-run moves is the
45
45
  * embedder's job.
46
- * - **Throws on unknown resources** — inside DBOS bodies the failure must
47
- * cross the step boundary as a throw so the step is durably recorded as
48
- * failed (see `lib/result.ts` house rules).
46
+ * - **Rejects on unknown resources** — inside DBOS bodies the awaited
47
+ * rejection must cross the step boundary as a throw so the step is durably
48
+ * recorded as failed (see `lib/result.ts` house rules).
49
+ *
50
+ * Async so that a realization can read host state (a marker file on a network
51
+ * mount) without a block of the event loop.
49
52
  */
50
- export type ResolveWorkspaceRoot = (resourceId: string) => string;
53
+ export type ResolveWorkspaceRoot = (resourceId: string) => Promise<string>;
51
54
  /** Result of resolving a workspace path against the canonical layout. */
52
55
  export type ResolvedPath = {
53
56
  readonly kind: "ok";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.43.1",
3
+ "version": "0.45.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "directory": "harness"
34
34
  },
35
35
  "scripts": {
36
- "build": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
36
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
37
37
  "design:fixture": "bun scripts/render-fixture.ts",
38
38
  "design:gallery": "bun scripts/render-gallery.ts",
39
39
  "format": "prettier --write src/",
@@ -41,12 +41,12 @@
41
41
  "gallery:data": "bash scripts/gallery-data.sh",
42
42
  "lint": "eslint .",
43
43
  "lint:python": "ruff check src/input-scan/decoder && ruff format --check src/input-scan/decoder",
44
- "prepare": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
44
+ "prepare": "tsc -p tsconfig.build.json && node scripts/copy-assets.mjs",
45
45
  "prepublishOnly": "bun run build && bun run smoke",
46
46
  "smoke": "node scripts/smoke.mjs",
47
47
  "test:full": "bash scripts/test-full.sh",
48
48
  "test:python": "python3 -m pytest src/input-scan/decoder -q",
49
- "typecheck": "tsc --noEmit"
49
+ "typecheck": "tsc -p tsconfig.json --noEmit"
50
50
  },
51
51
  "dependencies": {
52
52
  "@ai-sdk/anthropic": "^4.0.62",