@openparachute/agent 0.2.3-rc.10 → 0.2.3-rc.11
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.
- package/package.json +1 -1
- package/src/backends/programmatic.ts +35 -2
- package/src/backends/registry.ts +94 -13
- package/src/backends/types.ts +44 -0
- package/src/daemon.ts +6 -0
- package/src/transports/vault.ts +8 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openparachute/agent",
|
|
3
|
-
"version": "0.2.3-rc.
|
|
3
|
+
"version": "0.2.3-rc.11",
|
|
4
4
|
"description": "Vault-native agents for Claude Code — a #agent/definition note + an inbound message becomes a sandboxed claude turn; the reply is written back as a note. Messaging gateway on :1941.",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"type": "module",
|
|
@@ -85,6 +85,7 @@ import type {
|
|
|
85
85
|
DeliverResult,
|
|
86
86
|
DeliverUsage,
|
|
87
87
|
InterimSink,
|
|
88
|
+
RunContext,
|
|
88
89
|
TurnSession,
|
|
89
90
|
} from "./types.ts";
|
|
90
91
|
|
|
@@ -327,6 +328,32 @@ export function buildProgrammaticClaudeArgs(opts: {
|
|
|
327
328
|
return argv;
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Render the {@link RunContext} as a concise, clearly-LABELED preamble to PREPEND to a turn's
|
|
333
|
+
* message (agent#162). A headless `claude -p` turn has no clock + no notion of which run it is,
|
|
334
|
+
* so the daemon hands it these facts (the real wall-clock, new-vs-resumed, why it fired, the
|
|
335
|
+
* prior turn count) — the agent then stamps ACCURATE times instead of fabricating them.
|
|
336
|
+
*
|
|
337
|
+
* It is a single fenced block clearly marked as daemon-injected runtime context (NOT the
|
|
338
|
+
* agent's own system prompt — that's untouched), then a blank line, then the real message. The
|
|
339
|
+
* `now` is always present; the rest are appended only when known. Returns the message UNCHANGED
|
|
340
|
+
* when `rc` is absent (additive — no behavior change for a caller that doesn't pass one).
|
|
341
|
+
*/
|
|
342
|
+
export function renderRunContext(message: string, rc: RunContext | undefined): string {
|
|
343
|
+
if (!rc) return message;
|
|
344
|
+
const parts: string[] = [`now=${rc.now}`, `session=${rc.session}`];
|
|
345
|
+
if (typeof rc.priorTurnCount === "number" && rc.priorTurnCount >= 0) {
|
|
346
|
+
// The NUMBER of this turn (1-based) = completed turns + 1 — what an agent stamps as "turn N".
|
|
347
|
+
parts.push(`turn=${rc.priorTurnCount + 1}`);
|
|
348
|
+
}
|
|
349
|
+
if (rc.firedBy) parts.push(`fired-by=${rc.firedBy}`);
|
|
350
|
+
const preamble =
|
|
351
|
+
`[Run context — injected by the agent daemon (this is the real runtime state, NOT your ` +
|
|
352
|
+
`system prompt). Use these for any timestamp/clock or "which run is this" reasoning instead ` +
|
|
353
|
+
`of guessing: ${parts.join(", ")}]`;
|
|
354
|
+
return `${preamble}\n\n${message}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
330
357
|
/** Read the full text of a (possibly null) byte stream; null/error → "". */
|
|
331
358
|
async function drainStream(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
332
359
|
if (!stream) return "";
|
|
@@ -414,6 +441,7 @@ export class ProgrammaticBackend implements AgentBackend {
|
|
|
414
441
|
session: TurnSession,
|
|
415
442
|
onInterim?: InterimSink,
|
|
416
443
|
attachments?: InboundAttachment[],
|
|
444
|
+
runContext?: RunContext,
|
|
417
445
|
): Promise<DeliverResult> {
|
|
418
446
|
const spec = handle.spec;
|
|
419
447
|
if (!spec) {
|
|
@@ -537,13 +565,18 @@ export class ProgrammaticBackend implements AgentBackend {
|
|
|
537
565
|
// per-agent even when the working dir is shared. Best-effort + isolated: a single
|
|
538
566
|
// attachment's fetch/stage failure logs + is SKIPPED (the turn still runs with the rest
|
|
539
567
|
// + the text). Absent/empty → no staging, no prompt change (today's behavior exactly).
|
|
540
|
-
|
|
568
|
+
// RUN CONTEXT (agent#162): prepend the daemon-injected runtime preamble (the real
|
|
569
|
+
// wall-clock + new/resumed + why-it-fired) so the headless turn reads ACCURATE facts
|
|
570
|
+
// instead of fabricating a clock. Done FIRST so the preamble sits at the very top of the
|
|
571
|
+
// prompt; attachments append after the (already-prefixed) message. Absent runContext →
|
|
572
|
+
// the message is unchanged (additive).
|
|
573
|
+
let turnMessage = renderRunContext(message, runContext);
|
|
541
574
|
if (attachments && attachments.length > 0) {
|
|
542
575
|
const staged = await this.stageAttachments(workspace, attachments, vaultArg);
|
|
543
576
|
if (staged.length > 0) {
|
|
544
577
|
const lines = staged.map((s) => `- ${s.absPath} (${s.mimeType})`);
|
|
545
578
|
turnMessage =
|
|
546
|
-
`${
|
|
579
|
+
`${turnMessage}\n\n[Attached files — read them as needed:\n${lines.join("\n")}\n]`;
|
|
547
580
|
}
|
|
548
581
|
}
|
|
549
582
|
|
package/src/backends/registry.ts
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
import type { AgentSpec, AgentMode } from "../sandbox/types.ts";
|
|
46
46
|
import { normalizeChannel } from "../sandbox/types.ts";
|
|
47
|
-
import type { AgentBackend, AgentHandle, InterimTurnEvent, TurnSession } from "./types.ts";
|
|
47
|
+
import type { AgentBackend, AgentHandle, InterimTurnEvent, RunContext, TurnSession } from "./types.ts";
|
|
48
48
|
import type { InboundAttachment } from "../transport.ts";
|
|
49
49
|
|
|
50
50
|
/**
|
|
@@ -90,12 +90,15 @@ export type WriteOutbound = (
|
|
|
90
90
|
reply: string,
|
|
91
91
|
inReplyTo?: string,
|
|
92
92
|
/**
|
|
93
|
-
* The
|
|
94
|
-
* link the outbound note carries (stamped into `metadata.thread`)
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
93
|
+
* The RESOLVABLE, MODE-CORRECT thread id this reply belongs to — the explicit
|
|
94
|
+
* definition→thread→message link the outbound note carries (stamped into `metadata.thread`),
|
|
95
|
+
* mirroring the callback `source_thread` fix (agent#124/#163). For multi-threaded it IS the
|
|
96
|
+
* per-fire thread note's leaf (`Threads/<channel>/<id>`); for single-threaded it is the
|
|
97
|
+
* DETERMINISTIC thread-NOTE id (`Threads/<channel>/<name>`), STABLE across turns — so an
|
|
98
|
+
* observer reading the outbound note's `metadata.thread` resolves the agent's ONE thread
|
|
99
|
+
* with `query-notes { id }`, instead of the per-turn correlation UUID that changed every
|
|
100
|
+
* run (the pre-#163 bug that looked like a fresh session each time). INBOUND-note stamping
|
|
101
|
+
* is deferred (those notes are externally written; see the PR notes).
|
|
99
102
|
*/
|
|
100
103
|
threadId?: string,
|
|
101
104
|
) => Promise<{ id?: string } | void>;
|
|
@@ -302,6 +305,46 @@ function delay(ms: number): Promise<void> {
|
|
|
302
305
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
303
306
|
}
|
|
304
307
|
|
|
308
|
+
/**
|
|
309
|
+
* The thread id to stamp into an OUTBOUND note's `metadata.thread` (agent#163) — the
|
|
310
|
+
* MODE-CORRECT, RESOLVABLE definition→thread→message link, mirroring the callback
|
|
311
|
+
* `source_thread` fix (agent#124):
|
|
312
|
+
*
|
|
313
|
+
* - SINGLE-THREADED — the resolvable thread-NOTE id (the deterministic
|
|
314
|
+
* `Threads/<safeChannel>/<safeName>` note), so an observer reading the outbound note's
|
|
315
|
+
* `metadata.thread` resolves the agent's ONE stable thread with `query-notes { id }`.
|
|
316
|
+
* The pre-#163 bug stamped the per-turn correlation UUID here, which changed every turn
|
|
317
|
+
* and resolved to nothing — misleading an observer into "a fresh session each run" when
|
|
318
|
+
* the single-threaded session is actually stable + resumed across turns.
|
|
319
|
+
* - MULTI-THREADED — the per-fire id (`turnThreadId`), which IS that fire's thread-note leaf
|
|
320
|
+
* (`Threads/<safeChannel>/<turnThreadId>`) — correct as-is: each fire is its own thread.
|
|
321
|
+
*
|
|
322
|
+
* Falls back to `turnThreadId` when no resolvable note id surfaced (no durable thread store
|
|
323
|
+
* wired, or the thread-note write failed) — never undefined, so the link is always stamped.
|
|
324
|
+
*/
|
|
325
|
+
export function outboundThreadId(
|
|
326
|
+
multiThreaded: boolean,
|
|
327
|
+
turnThreadId: string,
|
|
328
|
+
threadNoteId: string | undefined,
|
|
329
|
+
): string {
|
|
330
|
+
if (multiThreaded) return turnThreadId;
|
|
331
|
+
return threadNoteId ?? turnThreadId;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Derive the run-context `fired-by` provenance (agent#162) from an inbound message's
|
|
336
|
+
* `sender` (the note's `metadata.sender`). A SCHEDULED job fire stamps `runner:<jobId>`
|
|
337
|
+
* (the runner's sender provenance) → reported as the job id so the agent knows it's a cron
|
|
338
|
+
* fire; anything else (a human / a delegated agent message) → `interactive`. Absent sender →
|
|
339
|
+
* undefined (the run context omits `fired-by`).
|
|
340
|
+
*/
|
|
341
|
+
export function runFiredBy(sender: string | undefined): string | undefined {
|
|
342
|
+
if (!sender) return undefined;
|
|
343
|
+
const m = /^runner:(.+)$/.exec(sender);
|
|
344
|
+
if (m) return `scheduled-job:${m[1]}`;
|
|
345
|
+
return "interactive";
|
|
346
|
+
}
|
|
347
|
+
|
|
305
348
|
/** A queued inbound message awaiting its serial turn. */
|
|
306
349
|
export interface QueuedMessage {
|
|
307
350
|
/** The inbound text handed to the `claude -p` turn as the prompt. */
|
|
@@ -343,6 +386,13 @@ export interface QueuedMessage {
|
|
|
343
386
|
* `Read` it. Absent/empty → no attachments (today's behavior unchanged).
|
|
344
387
|
*/
|
|
345
388
|
attachments?: InboundAttachment[];
|
|
389
|
+
/**
|
|
390
|
+
* WHO/WHAT sent this inbound (the note's `metadata.sender`) — used ONLY to derive the
|
|
391
|
+
* run-context `fired-by` provenance the daemon injects into the turn (agent#162): a
|
|
392
|
+
* SCHEDULED job fire stamps `runner:<jobId>`, an interactive/delegated message stamps
|
|
393
|
+
* something else. Absent → the run context omits `fired-by`. Carries no routing meaning.
|
|
394
|
+
*/
|
|
395
|
+
sender?: string;
|
|
346
396
|
}
|
|
347
397
|
|
|
348
398
|
/** A registered programmatic agent's live status (surfaced in /health + the list). */
|
|
@@ -806,7 +856,11 @@ export class ProgrammaticAgentRegistry {
|
|
|
806
856
|
// start+end never double-count. Best-effort: a start-ensure write failure is logged
|
|
807
857
|
// (inside recordThread) and the turn STILL runs — a missing/stale working note must
|
|
808
858
|
// never strand the queue or skip the turn.
|
|
809
|
-
|
|
859
|
+
// Capture the start-ensure's WRITTEN thread-note id. For single-threaded this is the
|
|
860
|
+
// DETERMINISTIC `Threads/<safeChannel>/<safeName>` note (the same every turn) — so a
|
|
861
|
+
// resolvable thread id is available BEFORE the turn runs, for the failure-note +
|
|
862
|
+
// outbound `metadata.thread` stamping (agent#163), even on a turn that fails early.
|
|
863
|
+
const startThreadNoteId = await this.recordThread(handle, msg, "working", "", startedAt, undefined, {
|
|
810
864
|
threadId: turnThreadId,
|
|
811
865
|
phase: "start",
|
|
812
866
|
// NO session on the start-ensure: it runs BEFORE claude, so claude may never
|
|
@@ -816,6 +870,26 @@ export class ProgrammaticAgentRegistry {
|
|
|
816
870
|
// the `end` record, and ONLY the id claude actually echoed (FIX 2). For a
|
|
817
871
|
// single-threaded resume turn the prior session is preserved by writeThread anyway.
|
|
818
872
|
});
|
|
873
|
+
// The MODE-CORRECT, RESOLVABLE thread id stamped into outbound + failure notes
|
|
874
|
+
// (agent#163): single-threaded → the deterministic thread-NOTE id (stable across turns);
|
|
875
|
+
// multi-threaded → the per-fire `turnThreadId`. Computed once from the start-ensure id so
|
|
876
|
+
// every path (early failure, ok, outbound-failure) stamps the same resolvable link.
|
|
877
|
+
const outThreadId = outboundThreadId(multiThreaded, turnThreadId, startThreadNoteId);
|
|
878
|
+
|
|
879
|
+
// RUN CONTEXT (agent#162): assemble the runtime facts a headless `claude -p` turn can't
|
|
880
|
+
// know — the REAL wall-clock (`startedAt`), whether this run CONTINUES a prior session
|
|
881
|
+
// (`turnSession.resume`) or starts fresh, and WHY it fired (a scheduled job vs an
|
|
882
|
+
// interactive/delegated message, from the inbound sender). The backend prepends these as
|
|
883
|
+
// a labeled preamble so the agent stamps ACCURATE times instead of fabricating them.
|
|
884
|
+
// (DEFERRED: `priorTurnCount` — the rolling turn_count lives in the transport's thread
|
|
885
|
+
// note and isn't surfaced back to the drain; wiring it is a follow-up. Until then the
|
|
886
|
+
// preamble omits the `turn=N` line — the `now`/session/fired-by trio is the floor.)
|
|
887
|
+
const firedBy = runFiredBy(msg.sender);
|
|
888
|
+
const runContext: RunContext = {
|
|
889
|
+
now: startedAt,
|
|
890
|
+
session: turnSession.resume ? "resumed" : "new",
|
|
891
|
+
...(firedBy ? { firedBy } : {}),
|
|
892
|
+
};
|
|
819
893
|
|
|
820
894
|
let result;
|
|
821
895
|
try {
|
|
@@ -830,6 +904,8 @@ export class ProgrammaticAgentRegistry {
|
|
|
830
904
|
// Phase 1: inbound attachments → the programmatic backend stages them into the
|
|
831
905
|
// agent's private workspace so the turn can Read them. Absent/empty → no staging.
|
|
832
906
|
msg.attachments,
|
|
907
|
+
// agent#162: the daemon-known runtime context (real clock, new/resumed, fired-by).
|
|
908
|
+
runContext,
|
|
833
909
|
);
|
|
834
910
|
} catch (err) {
|
|
835
911
|
// The backend contract is failure-as-VALUE, never a throw — but defend so a
|
|
@@ -854,8 +930,8 @@ export class ProgrammaticAgentRegistry {
|
|
|
854
930
|
});
|
|
855
931
|
this.emitTurnEvent(channel, { kind: "error", error: reason });
|
|
856
932
|
// Post a user-facing failure note so the channel shows SOMETHING (not a silent
|
|
857
|
-
// no-reply) — best-effort.
|
|
858
|
-
await this.postFailureNote(channel, msg.inReplyTo,
|
|
933
|
+
// no-reply) — best-effort. Stamp the MODE-CORRECT resolvable thread id (agent#163).
|
|
934
|
+
await this.postFailureNote(channel, msg.inReplyTo, outThreadId, reason);
|
|
859
935
|
// CALLBACK on the failure too — an orchestrator MUST learn its sub-task failed, not
|
|
860
936
|
// hang waiting forever. No outbound note was produced, so no `source_message`; the
|
|
861
937
|
// RESOLVABLE thread-note id (written above) is `source_thread` so the orchestrator can
|
|
@@ -886,8 +962,8 @@ export class ProgrammaticAgentRegistry {
|
|
|
886
962
|
});
|
|
887
963
|
this.emitTurnEvent(channel, { kind: "error", error: result.error });
|
|
888
964
|
// Post a user-facing failure note so the channel shows SOMETHING (not a silent
|
|
889
|
-
// no-reply) — best-effort.
|
|
890
|
-
await this.postFailureNote(channel, msg.inReplyTo,
|
|
965
|
+
// no-reply) — best-effort. Stamp the MODE-CORRECT resolvable thread id (agent#163).
|
|
966
|
+
await this.postFailureNote(channel, msg.inReplyTo, outThreadId, result.error);
|
|
891
967
|
// CALLBACK on the failure-as-value too (status:error) — the orchestrator learns the
|
|
892
968
|
// sub-task failed and can react. No delivered reply, so no `source_message`; the
|
|
893
969
|
// RESOLVABLE thread-note id (written above) is `source_thread` (agent#124).
|
|
@@ -931,7 +1007,12 @@ export class ProgrammaticAgentRegistry {
|
|
|
931
1007
|
channel,
|
|
932
1008
|
result.reply,
|
|
933
1009
|
msg.inReplyTo,
|
|
934
|
-
|
|
1010
|
+
// agent#163: stamp the MODE-CORRECT, RESOLVABLE thread id into the outbound note's
|
|
1011
|
+
// `metadata.thread` — single-threaded → the deterministic thread-NOTE id (stable
|
|
1012
|
+
// across turns; an observer resolves the ONE thread), multi-threaded → the per-fire
|
|
1013
|
+
// id. NOT the per-turn correlation UUID (the pre-#163 bug that looked like a fresh
|
|
1014
|
+
// session every run).
|
|
1015
|
+
outThreadId,
|
|
935
1016
|
);
|
|
936
1017
|
if (delivered.ok) sourceMessage = delivered.noteId;
|
|
937
1018
|
if (!delivered.ok) {
|
package/src/backends/types.ts
CHANGED
|
@@ -84,6 +84,43 @@ export interface TurnSession {
|
|
|
84
84
|
resume: boolean;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* RUN CONTEXT for one turn (agent#162) — the runtime facts a programmatic `claude -p` turn
|
|
89
|
+
* otherwise has NO way to know, so an agent stops FABRICATING them. A headless `-p` turn has
|
|
90
|
+
* no clock and no notion of "which run this is": uni-weaver was openly inventing report
|
|
91
|
+
* timestamps (a fixed `10:05` slot, the date "derived from context") because it couldn't read
|
|
92
|
+
* a real clock mid-run. The daemon KNOWS these facts at dispatch time, so it injects them into
|
|
93
|
+
* the turn (a concise, clearly-labeled preamble the agent reads) rather than letting the agent
|
|
94
|
+
* guess. Cheap, and it removes a whole class of fabricated-time confusion.
|
|
95
|
+
*
|
|
96
|
+
* The backend renders this as a SHORT preamble prepended to the turn message — it never
|
|
97
|
+
* mangles the agent's own system-prompt semantics. ADDITIVE: a caller that omits it leaves the
|
|
98
|
+
* turn message exactly as before.
|
|
99
|
+
*/
|
|
100
|
+
export interface RunContext {
|
|
101
|
+
/** The REAL wall-clock at dispatch (ISO 8601) — the authoritative clock the turn lacks. */
|
|
102
|
+
now: string;
|
|
103
|
+
/**
|
|
104
|
+
* Whether this turn CONTINUES a prior conversation (`resumed`, single-threaded turn 2+) or
|
|
105
|
+
* STARTS a fresh one (`new`, the first turn / every multi-threaded fire) — the cheap "which
|
|
106
|
+
* run is this" signal the daemon already resolved (`TurnSession.resume`).
|
|
107
|
+
*/
|
|
108
|
+
session: "new" | "resumed";
|
|
109
|
+
/**
|
|
110
|
+
* WHY this turn is running (provenance): a SCHEDULED job fire stamps `runner:<jobId>` (the
|
|
111
|
+
* runner's sender provenance) → reported as the job id; anything else is an interactive /
|
|
112
|
+
* delegated message → `interactive`. Lets a scheduled agent know it's a cron fire vs a live
|
|
113
|
+
* reply. Absent when the inbound carried no sender.
|
|
114
|
+
*/
|
|
115
|
+
firedBy?: string;
|
|
116
|
+
/**
|
|
117
|
+
* The thread's COMPLETED turn count BEFORE this turn (single-threaded's rolling counter;
|
|
118
|
+
* 0 on the first turn). Best-effort — omitted when the daemon can't cheaply resolve it
|
|
119
|
+
* (no durable thread store). So the agent can stamp "turn N" accurately.
|
|
120
|
+
*/
|
|
121
|
+
priorTurnCount?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
87
124
|
/**
|
|
88
125
|
* An opaque handle to a started agent, returned by {@link AgentBackend.start} and
|
|
89
126
|
* passed back to `deliver`/`stop`/`status`. The only field the seam itself depends
|
|
@@ -201,6 +238,12 @@ export interface AgentBackend {
|
|
|
201
238
|
* a safe basename) before the turn and appends a workspace-relative pointer to the
|
|
202
239
|
* prompt, so the `claude -p` turn can `Read` them. ADDITIVE — absent/empty → no
|
|
203
240
|
* staging, the turn behaves exactly as before.
|
|
241
|
+
*
|
|
242
|
+
* `runContext` (optional, agent#162) is the runtime context the daemon knows but a headless
|
|
243
|
+
* `-p` turn can't (the real wall-clock, whether this run is new vs resumed, why it fired).
|
|
244
|
+
* The programmatic backend prepends it as a concise, clearly-labeled preamble to the turn
|
|
245
|
+
* message so the agent stamps ACCURATE times instead of fabricating them. ADDITIVE — omitted
|
|
246
|
+
* → the turn message is exactly as before.
|
|
204
247
|
*/
|
|
205
248
|
deliver(
|
|
206
249
|
handle: AgentHandle,
|
|
@@ -208,6 +251,7 @@ export interface AgentBackend {
|
|
|
208
251
|
session: TurnSession,
|
|
209
252
|
onInterim?: InterimSink,
|
|
210
253
|
attachments?: InboundAttachment[],
|
|
254
|
+
runContext?: RunContext,
|
|
211
255
|
): Promise<DeliverResult>;
|
|
212
256
|
|
|
213
257
|
/**
|
package/src/daemon.ts
CHANGED
|
@@ -342,6 +342,9 @@ export function contextFor(
|
|
|
342
342
|
// Phase 1: carry inbound file attachments through to the turn (the programmatic
|
|
343
343
|
// backend stages them into the agent's private workspace so the turn can Read them).
|
|
344
344
|
...(msg.attachments && msg.attachments.length > 0 ? { attachments: msg.attachments } : {}),
|
|
345
|
+
// agent#162: carry the inbound sender so the drain can derive the run-context
|
|
346
|
+
// `fired-by` (a scheduled `runner:<jobId>` fire vs an interactive/delegated message).
|
|
347
|
+
...(msg.meta?.sender ? { sender: msg.meta.sender } : {}),
|
|
345
348
|
});
|
|
346
349
|
return;
|
|
347
350
|
}
|
|
@@ -366,6 +369,9 @@ export function contextFor(
|
|
|
366
369
|
// Phase 1: carry inbound attachments through the pending buffer too, so a turn
|
|
367
370
|
// that runs on register() still stages them.
|
|
368
371
|
...(msg.attachments && msg.attachments.length > 0 ? { attachments: msg.attachments } : {}),
|
|
372
|
+
// agent#162: carry the sender through the pending buffer too, so a turn that runs on
|
|
373
|
+
// register() still derives the right run-context `fired-by`.
|
|
374
|
+
...(msg.meta?.sender ? { sender: msg.meta.sender } : {}),
|
|
369
375
|
});
|
|
370
376
|
if (outcome === "queued") return;
|
|
371
377
|
// outcome === "unknown" — not an expected programmatic channel. It may still be a
|
package/src/transports/vault.ts
CHANGED
|
@@ -761,11 +761,14 @@ export class VaultTransport implements Transport {
|
|
|
761
761
|
// Thread the reply to the inbound note id when the bridge passes it through.
|
|
762
762
|
const inReplyTo = args.meta?.in_reply_to;
|
|
763
763
|
if (inReplyTo) metadata.in_reply_to = inReplyTo;
|
|
764
|
-
// The explicit definition→thread→message link: stamp the outbound note with its
|
|
765
|
-
// id (the programmatic worker passes
|
|
766
|
-
// multi-threaded turn this IS the per-fire `#agent/thread`
|
|
767
|
-
// single-threaded turn it's
|
|
768
|
-
// (
|
|
764
|
+
// The explicit definition→thread→message link: stamp the outbound note with its
|
|
765
|
+
// RESOLVABLE, mode-correct thread id (the programmatic worker passes it through
|
|
766
|
+
// `meta.thread`, agent#163). For a multi-threaded turn this IS the per-fire `#agent/thread`
|
|
767
|
+
// note's leaf; for a single-threaded turn it's the DETERMINISTIC thread-NOTE id
|
|
768
|
+
// (`Threads/<channel>/<name>`), STABLE across turns — so an observer resolves the agent's
|
|
769
|
+
// ONE thread from `metadata.thread`, not a per-turn UUID that changed every run.
|
|
770
|
+
// INBOUND-note stamping is deferred (those notes are written externally, before the turn
|
|
771
|
+
// knows its thread).
|
|
769
772
|
const threadId = args.meta?.thread;
|
|
770
773
|
if (threadId) metadata.thread = threadId;
|
|
771
774
|
|