harnery 0.9.0 → 0.10.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.
- package/README.md +1 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +6 -0
- package/dist/commands/agents.d.ts +24 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +244 -13
- package/dist/commands/backup.d.ts +5 -4
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +15 -14
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +27 -1
- package/dist/commands/claude-desktop.d.ts +19 -0
- package/dist/commands/claude-desktop.d.ts.map +1 -0
- package/dist/commands/claude-desktop.js +168 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +149 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/eml.d.ts +32 -0
- package/dist/commands/eml.d.ts.map +1 -1
- package/dist/commands/eml.js +16 -3
- package/dist/commands/harness.d.ts +11 -0
- package/dist/commands/harness.d.ts.map +1 -0
- package/dist/commands/harness.js +118 -0
- package/dist/commands/presence.d.ts +9 -4
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +88 -5
- package/dist/commands/relay.d.ts +9 -0
- package/dist/commands/relay.d.ts.map +1 -0
- package/dist/commands/relay.js +143 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +5 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +8 -15
- package/dist/core/agents/canonical-emit.d.ts +15 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +41 -3
- package/dist/core/agents/cli.js +4 -2
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +44 -3
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +26 -1
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +4 -37
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +16 -43
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-projector.js +11 -2
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +3 -3
- package/dist/core/agents/state/names.d.ts +38 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +46 -7
- package/dist/core/agents/state/pidmap.d.ts +5 -1
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +58 -2
- package/dist/core/agents/state/stale-sweep.d.ts +3 -2
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +6 -6
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +191 -25
- package/dist/core/context/index.d.ts +144 -0
- package/dist/core/context/index.d.ts.map +1 -0
- package/dist/core/context/index.js +380 -0
- package/dist/core/harnesses/bench.d.ts +29 -0
- package/dist/core/harnesses/bench.d.ts.map +1 -0
- package/dist/core/harnesses/bench.js +151 -0
- package/dist/core/harnesses/index.d.ts +9 -0
- package/dist/core/harnesses/index.d.ts.map +1 -0
- package/dist/core/harnesses/index.js +4 -0
- package/dist/core/harnesses/profiles.d.ts +62 -0
- package/dist/core/harnesses/profiles.d.ts.map +1 -0
- package/dist/core/harnesses/profiles.js +115 -0
- package/dist/core/harnesses/registry.d.ts +15 -0
- package/dist/core/harnesses/registry.d.ts.map +1 -0
- package/dist/core/harnesses/registry.js +113 -0
- package/dist/core/harnesses/types.d.ts +64 -0
- package/dist/core/harnesses/types.d.ts.map +1 -0
- package/dist/core/harnesses/types.js +20 -0
- package/dist/core/hooks/cli.js +280 -63
- package/dist/core/hooks/events/schema.d.ts +38 -1
- package/dist/core/hooks/events/schema.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.d.ts.map +1 -1
- package/dist/core/hooks/harness/events.js +3 -0
- package/dist/core/hooks/harness/parse.d.ts +1 -1
- package/dist/core/hooks/harness/parse.d.ts.map +1 -1
- package/dist/core/hooks/harness/parse.js +4 -0
- package/dist/core/presence/blob.d.ts +40 -0
- package/dist/core/presence/blob.d.ts.map +1 -0
- package/dist/core/presence/blob.js +91 -0
- package/dist/core/presence/git.d.ts +68 -0
- package/dist/core/presence/git.d.ts.map +1 -0
- package/dist/core/presence/git.js +173 -0
- package/dist/core/presence/index.d.ts +74 -0
- package/dist/core/presence/index.d.ts.map +1 -0
- package/dist/core/presence/index.js +224 -0
- package/dist/core/presence/relay-client.d.ts +39 -0
- package/dist/core/presence/relay-client.d.ts.map +1 -0
- package/dist/core/presence/relay-client.js +295 -0
- package/dist/core/presence/relay-protocol.d.ts +94 -0
- package/dist/core/presence/relay-protocol.d.ts.map +1 -0
- package/dist/core/presence/relay-protocol.js +169 -0
- package/dist/core/workflow/billing.d.ts +1 -1
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +7 -1
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +11 -2
- package/dist/core/workflow/harnesses.d.ts +3 -3
- package/dist/core/workflow/harnesses.d.ts.map +1 -1
- package/dist/core/workflow/harnesses.js +8 -17
- package/dist/core/workflow/spawn-claude.d.ts +4 -1
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +39 -18
- package/dist/core/workflow/spawn-codex.d.ts +4 -1
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +44 -22
- package/dist/core/workflow/spawn-cursor.d.ts +4 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +35 -16
- package/dist/core/workflow/types.d.ts +9 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/lib/browser/client.d.ts +16 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +22 -0
- package/dist/lib/browser/index.d.ts +1 -0
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -0
- package/dist/lib/browser/launch-args.d.ts +21 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -0
- package/dist/lib/browser/launch-args.js +32 -0
- package/dist/lib/claude-desktop.d.ts +105 -0
- package/dist/lib/claude-desktop.d.ts.map +1 -0
- package/dist/lib/claude-desktop.js +217 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +5 -2
- package/dist/lib/identities/assume.d.ts +51 -0
- package/dist/lib/identities/assume.d.ts.map +1 -0
- package/dist/lib/identities/assume.js +274 -0
- package/dist/lib/identities/index.d.ts +7 -7
- package/dist/lib/identities/index.d.ts.map +1 -1
- package/dist/lib/identities/index.js +24 -24
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +15 -2
- package/package.json +12 -1
- package/schemas/config.schema.json +88 -19
- package/src/commander.ts +6 -0
- package/src/commands/agents.ts +280 -14
- package/src/commands/backup.ts +17 -16
- package/src/commands/browse.ts +32 -0
- package/src/commands/claude-desktop.ts +215 -0
- package/src/commands/context.ts +174 -1
- package/src/commands/doctor.ts +2 -3
- package/src/commands/eml.ts +22 -4
- package/src/commands/harness.ts +147 -0
- package/src/commands/presence.ts +111 -5
- package/src/commands/relay.ts +160 -0
- package/src/commands/sync.ts +4 -0
- package/src/commands/workflow.ts +9 -18
- package/src/core/agents/canonical-emit.ts +41 -3
- package/src/core/agents/cli.ts +4 -2
- package/src/core/agents/render/prompt-context.ts +48 -3
- package/src/core/agents/render/session-context.ts +27 -1
- package/src/core/agents/rules/commit-conflict.ts +4 -33
- package/src/core/agents/session-events.ts +15 -39
- package/src/core/agents/state/heartbeat-projector.ts +10 -1
- package/src/core/agents/state/heartbeat-writer.ts +3 -2
- package/src/core/agents/state/names.ts +68 -9
- package/src/core/agents/state/pidmap.ts +59 -2
- package/src/core/agents/state/stale-sweep.ts +6 -11
- package/src/core/config.ts +248 -24
- package/src/core/context/index.ts +575 -0
- package/src/core/harnesses/bench.ts +214 -0
- package/src/core/harnesses/index.ts +31 -0
- package/src/core/harnesses/profiles.ts +141 -0
- package/src/core/harnesses/registry.ts +140 -0
- package/src/core/harnesses/types.ts +90 -0
- package/src/core/hooks/cli.ts +331 -62
- package/src/core/hooks/events/schema.ts +68 -0
- package/src/core/hooks/harness/events.ts +3 -0
- package/src/core/hooks/harness/parse.ts +7 -1
- package/src/core/presence/blob.ts +125 -0
- package/src/core/presence/git.ts +191 -0
- package/src/core/presence/index.ts +274 -0
- package/src/core/presence/relay-client.ts +322 -0
- package/src/core/presence/relay-protocol.ts +246 -0
- package/src/core/workflow/billing.ts +8 -2
- package/src/core/workflow/engine.ts +11 -2
- package/src/core/workflow/harnesses.ts +14 -19
- package/src/core/workflow/spawn-claude.ts +39 -20
- package/src/core/workflow/spawn-codex.ts +47 -28
- package/src/core/workflow/spawn-cursor.ts +37 -18
- package/src/core/workflow/types.ts +9 -2
- package/src/lib/browser/client.ts +37 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/launch-args.ts +33 -0
- package/src/lib/claude-desktop.ts +293 -0
- package/src/lib/docs-lint.ts +5 -2
- package/src/lib/identities/assume.ts +363 -0
- package/src/lib/identities/index.ts +28 -24
- package/src/lib/instructions/templates.ts +16 -2
package/src/core/hooks/cli.ts
CHANGED
|
@@ -27,6 +27,17 @@ import { consumeSince, writeCursor } from "../agents/events/consume.ts";
|
|
|
27
27
|
import { evaluateStopHook } from "../agents/rules/stop-hook.ts";
|
|
28
28
|
import { projectHeartbeats } from "../agents/state/heartbeat-projector.ts";
|
|
29
29
|
import { shellMutationPaths } from "../agents/state/shell-mutation.ts";
|
|
30
|
+
import {
|
|
31
|
+
checkpointContext,
|
|
32
|
+
completeContextRecovery,
|
|
33
|
+
extractContextSample,
|
|
34
|
+
markContextCompactionCompleted,
|
|
35
|
+
type PreparedContextRecovery,
|
|
36
|
+
prepareContextRecovery,
|
|
37
|
+
readContextState,
|
|
38
|
+
recordContextSample,
|
|
39
|
+
} from "../context/index.ts";
|
|
40
|
+
import { ensureRelayDaemon, fetchPresence, publishPresence } from "../presence/index.ts";
|
|
30
41
|
import {
|
|
31
42
|
captureImages,
|
|
32
43
|
detectPresence,
|
|
@@ -386,9 +397,49 @@ function buildEventData(
|
|
|
386
397
|
...(summary.truncated ? { truncated: true } : {}),
|
|
387
398
|
};
|
|
388
399
|
}
|
|
400
|
+
|
|
401
|
+
case "context.compaction.started": {
|
|
402
|
+
const metadata = objectRecord(p?.raw.compact_metadata);
|
|
403
|
+
return {
|
|
404
|
+
trigger: stringField(p?.raw.trigger) ?? stringField(metadata?.trigger),
|
|
405
|
+
pre_tokens:
|
|
406
|
+
numberField(p?.raw.pre_tokens) ??
|
|
407
|
+
numberField(metadata?.pre_tokens) ??
|
|
408
|
+
numberField(metadata?.pre_compact_tokens),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
case "context.compaction.completed": {
|
|
413
|
+
const metadata = objectRecord(p?.raw.compact_metadata);
|
|
414
|
+
return {
|
|
415
|
+
trigger: stringField(p?.raw.trigger) ?? stringField(metadata?.trigger),
|
|
416
|
+
pre_tokens:
|
|
417
|
+
numberField(p?.raw.pre_tokens) ??
|
|
418
|
+
numberField(metadata?.pre_tokens) ??
|
|
419
|
+
numberField(metadata?.pre_compact_tokens),
|
|
420
|
+
post_tokens:
|
|
421
|
+
numberField(p?.raw.post_tokens) ??
|
|
422
|
+
numberField(metadata?.post_tokens) ??
|
|
423
|
+
numberField(metadata?.post_compact_tokens),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
389
426
|
}
|
|
390
427
|
}
|
|
391
428
|
|
|
429
|
+
function objectRecord(value: unknown): Record<string, unknown> | undefined {
|
|
430
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
431
|
+
? (value as Record<string, unknown>)
|
|
432
|
+
: undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function stringField(value: unknown): string | undefined {
|
|
436
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function numberField(value: unknown): number | undefined {
|
|
440
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
392
443
|
async function main(): Promise<number> {
|
|
393
444
|
const { eventName, extra } = parseArgv(process.argv.slice(2));
|
|
394
445
|
const harness = detectHarness(process.argv.slice(2));
|
|
@@ -489,6 +540,84 @@ async function main(): Promise<number> {
|
|
|
489
540
|
event_id: envelope.event_id,
|
|
490
541
|
});
|
|
491
542
|
|
|
543
|
+
// Context telemetry is opportunistic and truthful: only persist a sample
|
|
544
|
+
// when the harness payload actually exposes usage/window data. Identical
|
|
545
|
+
// measurements are de-duplicated before they reach the canonical stream.
|
|
546
|
+
if (payload?.raw) {
|
|
547
|
+
try {
|
|
548
|
+
const sample = extractContextSample(payload.raw, {
|
|
549
|
+
sessionId,
|
|
550
|
+
harness,
|
|
551
|
+
model: payload.model,
|
|
552
|
+
source: "hook",
|
|
553
|
+
confidence: "reported",
|
|
554
|
+
});
|
|
555
|
+
if (sample) {
|
|
556
|
+
const recorded = recordContextSample(coordRoot, owner.instance_id, sample);
|
|
557
|
+
if (recorded.changed) {
|
|
558
|
+
emit(coordRoot, {
|
|
559
|
+
event_type: "context.sampled",
|
|
560
|
+
instance_id: owner.instance_id,
|
|
561
|
+
session_id: sessionId,
|
|
562
|
+
harness,
|
|
563
|
+
data: {
|
|
564
|
+
model: sample.model,
|
|
565
|
+
used_tokens: sample.used_tokens,
|
|
566
|
+
window_tokens: sample.window_tokens,
|
|
567
|
+
used_percent: sample.used_percent,
|
|
568
|
+
telemetry_source: sample.source,
|
|
569
|
+
confidence: sample.confidence,
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
logError(coordRoot, err, { phase: "context-sample" });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// A native pre-compaction signal is the safest available point to capture
|
|
580
|
+
// external work state. The operation is idempotent until recovery, so a
|
|
581
|
+
// harness retry cannot create a storm of near-identical capsules.
|
|
582
|
+
if (norm.event_type === "context.compaction.started") {
|
|
583
|
+
try {
|
|
584
|
+
const checkpoint = checkpointContext(coordRoot, {
|
|
585
|
+
sessionId,
|
|
586
|
+
instanceId: owner.instance_id,
|
|
587
|
+
harness,
|
|
588
|
+
cwd: payload?.cwd ?? process.cwd(),
|
|
589
|
+
reason: "pre_compact",
|
|
590
|
+
model: payload?.model,
|
|
591
|
+
});
|
|
592
|
+
emit(coordRoot, {
|
|
593
|
+
event_type: "context.checkpoint.created",
|
|
594
|
+
instance_id: owner.instance_id,
|
|
595
|
+
session_id: sessionId,
|
|
596
|
+
harness,
|
|
597
|
+
data: {
|
|
598
|
+
capsule_id: checkpoint.capsule.capsule_id,
|
|
599
|
+
generation: checkpoint.capsule.generation,
|
|
600
|
+
path: checkpoint.state.latest_capsule ?? "",
|
|
601
|
+
reason: checkpoint.capsule.reason,
|
|
602
|
+
reused: checkpoint.reused,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
} catch (err) {
|
|
606
|
+
logError(coordRoot, err, { phase: "context-checkpoint" });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (norm.event_type === "context.compaction.completed") {
|
|
611
|
+
try {
|
|
612
|
+
markContextCompactionCompleted(coordRoot, {
|
|
613
|
+
sessionId,
|
|
614
|
+
instanceId: owner.instance_id,
|
|
615
|
+
});
|
|
616
|
+
} catch (err) {
|
|
617
|
+
logError(coordRoot, err, { phase: "context-compaction-completed" });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
492
621
|
// Phase 8: SessionStart post-emit: project the event so the heartbeat
|
|
493
622
|
// lands synchronously, run stale-sweep, and emit the harness-shaped
|
|
494
623
|
// systemMessage JSON (peer table + wiring check + council invites).
|
|
@@ -507,11 +636,61 @@ async function main(): Promise<number> {
|
|
|
507
636
|
} catch (err) {
|
|
508
637
|
logError(coordRoot, err, { phase: "session-start-image-janitor" });
|
|
509
638
|
}
|
|
639
|
+
let recovery: PreparedContextRecovery | null = null;
|
|
640
|
+
if (payload?.source === "compact") {
|
|
641
|
+
try {
|
|
642
|
+
emit(coordRoot, {
|
|
643
|
+
event_type: "context.compaction.completed",
|
|
644
|
+
instance_id: owner.instance_id,
|
|
645
|
+
session_id: sessionId,
|
|
646
|
+
harness,
|
|
647
|
+
data: { trigger: "auto" },
|
|
648
|
+
});
|
|
649
|
+
markContextCompactionCompleted(coordRoot, {
|
|
650
|
+
sessionId,
|
|
651
|
+
instanceId: owner.instance_id,
|
|
652
|
+
});
|
|
653
|
+
recovery = prepareContextRecovery(coordRoot, {
|
|
654
|
+
instanceId: owner.instance_id,
|
|
655
|
+
sessionId,
|
|
656
|
+
cwd: payload?.cwd ?? process.cwd(),
|
|
657
|
+
});
|
|
658
|
+
} catch (err) {
|
|
659
|
+
logError(coordRoot, err, { phase: "context-recovery-session-start" });
|
|
660
|
+
}
|
|
661
|
+
}
|
|
510
662
|
try {
|
|
511
|
-
await emitSessionStartSystemMessage(
|
|
663
|
+
const injected = await emitSessionStartSystemMessage(
|
|
664
|
+
coordRoot,
|
|
665
|
+
owner.instance_id,
|
|
666
|
+
sessionId,
|
|
667
|
+
data,
|
|
668
|
+
harness,
|
|
669
|
+
recovery?.briefing ?? "",
|
|
670
|
+
);
|
|
671
|
+
if (injected && recovery) {
|
|
672
|
+
completeRecoveryInjection(
|
|
673
|
+
coordRoot,
|
|
674
|
+
owner.instance_id,
|
|
675
|
+
sessionId,
|
|
676
|
+
harness,
|
|
677
|
+
recovery,
|
|
678
|
+
"SessionStart",
|
|
679
|
+
);
|
|
680
|
+
}
|
|
512
681
|
} catch (err) {
|
|
513
682
|
logError(coordRoot, err, { phase: "session-start-systemMessage" });
|
|
514
683
|
}
|
|
684
|
+
// Cross-machine presence (ADR 0016): announce this machine's sessions and
|
|
685
|
+
// pull peers'. Both are throttled + fail-silent; network runs detached.
|
|
686
|
+
// When a relay is configured, also make sure the live-socket daemon runs.
|
|
687
|
+
try {
|
|
688
|
+
publishPresence(coordRoot);
|
|
689
|
+
fetchPresence(coordRoot);
|
|
690
|
+
ensureRelayDaemon(coordRoot);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
logError(coordRoot, err, { phase: "session-start-presence" });
|
|
693
|
+
}
|
|
515
694
|
}
|
|
516
695
|
|
|
517
696
|
// Phase 8: SessionEnd cleanup: delete heartbeat + pid-map rows. Harness-
|
|
@@ -528,6 +707,13 @@ async function main(): Promise<number> {
|
|
|
528
707
|
scratchArchive(coordRoot, owner.instance_id);
|
|
529
708
|
runSessionSyncExtension(coordRoot, true);
|
|
530
709
|
}
|
|
710
|
+
// Presence: publish the post-cleanup state so peers see this session gone
|
|
711
|
+
// promptly rather than waiting out the remote-stale window.
|
|
712
|
+
try {
|
|
713
|
+
publishPresence(coordRoot);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
logError(coordRoot, err, { phase: "session-end-presence" });
|
|
716
|
+
}
|
|
531
717
|
}
|
|
532
718
|
|
|
533
719
|
// Phase 8: SubagentStart: sync-project to create the subagent heartbeat,
|
|
@@ -591,8 +777,47 @@ async function main(): Promise<number> {
|
|
|
591
777
|
const prompt = (payload?.raw?.prompt as string | undefined) ?? "";
|
|
592
778
|
if (prompt) detectPresence(prompt);
|
|
593
779
|
}
|
|
780
|
+
// Presence: throttled pull of peer machines' presence refs (default 60s
|
|
781
|
+
// interval; the systemMessage below reads whatever is locally known).
|
|
594
782
|
try {
|
|
595
|
-
|
|
783
|
+
fetchPresence(coordRoot);
|
|
784
|
+
} catch (err) {
|
|
785
|
+
logError(coordRoot, err, { phase: "user-prompt-submit-presence" });
|
|
786
|
+
}
|
|
787
|
+
let recovery: PreparedContextRecovery | null = null;
|
|
788
|
+
const continuityState = readContextState(coordRoot, sessionId);
|
|
789
|
+
if (
|
|
790
|
+
continuityState?.phase === "checkpointed" &&
|
|
791
|
+
continuityState.compaction_completed_at !== undefined
|
|
792
|
+
) {
|
|
793
|
+
try {
|
|
794
|
+
recovery = prepareContextRecovery(coordRoot, {
|
|
795
|
+
instanceId: owner.instance_id,
|
|
796
|
+
sessionId,
|
|
797
|
+
cwd: payload?.cwd ?? process.cwd(),
|
|
798
|
+
});
|
|
799
|
+
} catch (err) {
|
|
800
|
+
logError(coordRoot, err, { phase: "context-recovery-user-prompt" });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
const injected = await emitUserPromptSubmitSystemMessage(
|
|
805
|
+
coordRoot,
|
|
806
|
+
owner.instance_id,
|
|
807
|
+
sessionId,
|
|
808
|
+
harness,
|
|
809
|
+
recovery?.briefing ?? "",
|
|
810
|
+
);
|
|
811
|
+
if (injected && recovery) {
|
|
812
|
+
completeRecoveryInjection(
|
|
813
|
+
coordRoot,
|
|
814
|
+
owner.instance_id,
|
|
815
|
+
sessionId,
|
|
816
|
+
harness,
|
|
817
|
+
recovery,
|
|
818
|
+
"UserPromptSubmit",
|
|
819
|
+
);
|
|
820
|
+
}
|
|
596
821
|
} catch (err) {
|
|
597
822
|
logError(coordRoot, err, { phase: "user-prompt-submit-systemMessage" });
|
|
598
823
|
}
|
|
@@ -645,6 +870,17 @@ async function main(): Promise<number> {
|
|
|
645
870
|
logError(coordRoot, err, { phase: "stop-projection" });
|
|
646
871
|
}
|
|
647
872
|
|
|
873
|
+
// Presence: publish AFTER the projection above so the blob carries this
|
|
874
|
+
// turn's fresh task/turn_summary/files. Change-batched + keepalive'd;
|
|
875
|
+
// the push itself runs detached. The relay daemon (when configured) is
|
|
876
|
+
// re-ensured here too — it self-exits on idle, and the next turn revives it.
|
|
877
|
+
try {
|
|
878
|
+
publishPresence(coordRoot);
|
|
879
|
+
ensureRelayDaemon(coordRoot);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
logError(coordRoot, err, { phase: "stop-presence" });
|
|
882
|
+
}
|
|
883
|
+
|
|
648
884
|
// Stop verdict (status-box + set-task gate). Direct in-process call: the
|
|
649
885
|
// rule lives in harnery. agent-hook already emitted this turn.stop (with
|
|
650
886
|
// status_box_present) above, so the evidence is in the stream.
|
|
@@ -1128,36 +1364,42 @@ async function emitUserPromptSubmitSystemMessage(
|
|
|
1128
1364
|
instanceId: string,
|
|
1129
1365
|
sessionId: string,
|
|
1130
1366
|
harness: Harness,
|
|
1131
|
-
|
|
1367
|
+
recoveryBriefing = "",
|
|
1368
|
+
): Promise<boolean> {
|
|
1132
1369
|
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
1133
|
-
|
|
1370
|
+
let additionalContext = "";
|
|
1134
1371
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1372
|
+
if (existsSync(agentCoordBin)) {
|
|
1373
|
+
// Look up the agent's name from its heartbeat (for council pending rendering).
|
|
1374
|
+
let agentName = "";
|
|
1375
|
+
try {
|
|
1376
|
+
const fs = require("node:fs") as typeof import("node:fs");
|
|
1377
|
+
const hbPath = join(coordRoot, ".harnery", "active", `${instanceId}.json`);
|
|
1378
|
+
if (fs.existsSync(hbPath)) {
|
|
1379
|
+
const hb = JSON.parse(fs.readFileSync(hbPath, "utf8")) as { name?: string };
|
|
1380
|
+
agentName = hb.name ?? "";
|
|
1381
|
+
}
|
|
1382
|
+
} catch {
|
|
1383
|
+
/* fall through with empty name; peer table still renders */
|
|
1143
1384
|
}
|
|
1144
|
-
} catch {
|
|
1145
|
-
/* fall through with empty name; peer table still renders */
|
|
1146
|
-
}
|
|
1147
1385
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if (
|
|
1386
|
+
const args = ["prompt-context", "--instance", instanceId, "--session", sessionId];
|
|
1387
|
+
if (agentName) args.push("--name", agentName);
|
|
1388
|
+
// Cursor + Codex sessions get the set-task staleness nudge. CC enforces it
|
|
1389
|
+
// via the Stop-hook transcript scan; the nudge replaces that for harnesses
|
|
1390
|
+
// that don't reliably expose a transcript_path during stop.
|
|
1391
|
+
if (harness === "cursor" || harness === "codex") args.push("--task-nudge");
|
|
1392
|
+
const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
|
|
1393
|
+
if (result.status === 0 && result.stdout) additionalContext = result.stdout.trim();
|
|
1394
|
+
}
|
|
1395
|
+
if (recoveryBriefing) {
|
|
1396
|
+
additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
|
|
1397
|
+
}
|
|
1398
|
+
if (!additionalContext) return false;
|
|
1158
1399
|
|
|
1159
1400
|
const { emitContext } = await import("./harness/output.ts");
|
|
1160
1401
|
emitContext(harness, "UserPromptSubmit", additionalContext);
|
|
1402
|
+
return true;
|
|
1161
1403
|
}
|
|
1162
1404
|
|
|
1163
1405
|
function emitSubagentStartContext(
|
|
@@ -1210,45 +1452,45 @@ async function emitSessionStartSystemMessage(
|
|
|
1210
1452
|
sessionId: string,
|
|
1211
1453
|
emittedData: Record<string, unknown>,
|
|
1212
1454
|
harness: Harness,
|
|
1213
|
-
|
|
1455
|
+
recoveryBriefing = "",
|
|
1456
|
+
): Promise<boolean> {
|
|
1214
1457
|
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
1215
|
-
|
|
1458
|
+
let additionalContext = "";
|
|
1459
|
+
if (existsSync(agentCoordBin)) {
|
|
1460
|
+
// Sync-project so the heartbeat exists for downstream readers (peer table,
|
|
1461
|
+
// wiring check, council invites).
|
|
1462
|
+
spawnSync(agentCoordBin, ["project"], { encoding: "utf8", timeout: 3000 });
|
|
1463
|
+
// Stale-sweep dead peers before rendering peer table.
|
|
1464
|
+
spawnSync(agentCoordBin, ["stale-sweep"], { encoding: "utf8", timeout: 3000 });
|
|
1465
|
+
|
|
1466
|
+
// SESSION_START activity log line, fired across all harnesses.
|
|
1467
|
+
const model = (emittedData.model as string | undefined) ?? "unknown";
|
|
1468
|
+
const source = (emittedData.source as string | undefined) ?? "startup";
|
|
1469
|
+
const platform = harnessPlatform(harness);
|
|
1470
|
+
spawnSync(
|
|
1471
|
+
agentCoordBin,
|
|
1472
|
+
[
|
|
1473
|
+
"log",
|
|
1474
|
+
`SESSION_START model=${model} source=${source} platform=${platform}`,
|
|
1475
|
+
"--instance",
|
|
1476
|
+
instanceId,
|
|
1477
|
+
],
|
|
1478
|
+
{ encoding: "utf8", timeout: 2000 },
|
|
1479
|
+
);
|
|
1216
1480
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
[
|
|
1230
|
-
"log",
|
|
1231
|
-
`SESSION_START model=${model} source=${source} platform=${platform}`,
|
|
1232
|
-
"--instance",
|
|
1233
|
-
instanceId,
|
|
1234
|
-
],
|
|
1235
|
-
{ encoding: "utf8", timeout: 2000 },
|
|
1236
|
-
);
|
|
1237
|
-
|
|
1238
|
-
// Render the systemMessage via agent-coord.
|
|
1239
|
-
const agentName = (emittedData.name as string | undefined) ?? "";
|
|
1240
|
-
const args = ["session-context", "--instance", instanceId, "--session", sessionId];
|
|
1241
|
-
if (agentName) args.push("--name", agentName);
|
|
1242
|
-
// The "You are agent-X." prefix in session-context renders unqualified by
|
|
1243
|
-
// default (claude-code-style). For cursor/codex the bash dispatchers add
|
|
1244
|
-
// a "(Cursor)" / "(Codex)" suffix; pass it through as --platform-label.
|
|
1245
|
-
if (harness !== "claude-code") {
|
|
1246
|
-
args.push("--platform-label", platform === "cursor" ? "Cursor" : "Codex");
|
|
1481
|
+
// Render the systemMessage via agent-coord.
|
|
1482
|
+
const agentName = (emittedData.name as string | undefined) ?? "";
|
|
1483
|
+
const args = ["session-context", "--instance", instanceId, "--session", sessionId];
|
|
1484
|
+
if (agentName) args.push("--name", agentName);
|
|
1485
|
+
// The "You are agent-X." prefix in session-context renders unqualified by
|
|
1486
|
+
// default (claude-code-style). For cursor/codex the bash dispatchers add
|
|
1487
|
+
// a "(Cursor)" / "(Codex)" suffix; pass it through as --platform-label.
|
|
1488
|
+
if (harness !== "claude-code") {
|
|
1489
|
+
args.push("--platform-label", platform === "cursor" ? "Cursor" : "Codex");
|
|
1490
|
+
}
|
|
1491
|
+
const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
|
|
1492
|
+
if (result.status === 0 && result.stdout) additionalContext = result.stdout.trim();
|
|
1247
1493
|
}
|
|
1248
|
-
const result = spawnSync(agentCoordBin, args, { encoding: "utf8", timeout: 3000 });
|
|
1249
|
-
if (result.status !== 0 || !result.stdout) return;
|
|
1250
|
-
let additionalContext = result.stdout.trim();
|
|
1251
|
-
if (!additionalContext) return;
|
|
1252
1494
|
|
|
1253
1495
|
// Effect (claude-code): merge the scratch recovery cue into the session-start
|
|
1254
1496
|
// context. Was a standalone additionalContext emission from the previous
|
|
@@ -1256,11 +1498,38 @@ async function emitSessionStartSystemMessage(
|
|
|
1256
1498
|
// entry, it folds in here.
|
|
1257
1499
|
if (harness === "claude-code") {
|
|
1258
1500
|
const cue = scratchRecoveryCue(coordRoot);
|
|
1259
|
-
if (cue) additionalContext =
|
|
1501
|
+
if (cue) additionalContext = [additionalContext, cue].filter(Boolean).join("\n\n");
|
|
1260
1502
|
}
|
|
1503
|
+
if (recoveryBriefing) {
|
|
1504
|
+
additionalContext = [additionalContext, recoveryBriefing].filter(Boolean).join("\n\n");
|
|
1505
|
+
}
|
|
1506
|
+
if (!additionalContext) return false;
|
|
1261
1507
|
|
|
1262
1508
|
const { emitContext } = await import("./harness/output.ts");
|
|
1263
1509
|
emitContext(harness, "SessionStart", additionalContext);
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function completeRecoveryInjection(
|
|
1514
|
+
coordRoot: string,
|
|
1515
|
+
instanceId: string,
|
|
1516
|
+
sessionId: string,
|
|
1517
|
+
harness: Harness,
|
|
1518
|
+
recovery: PreparedContextRecovery,
|
|
1519
|
+
injectionEvent: "SessionStart" | "UserPromptSubmit",
|
|
1520
|
+
): void {
|
|
1521
|
+
completeContextRecovery(coordRoot, { sessionId, instanceId });
|
|
1522
|
+
emit(coordRoot, {
|
|
1523
|
+
event_type: "context.recovery.injected",
|
|
1524
|
+
instance_id: instanceId,
|
|
1525
|
+
session_id: sessionId,
|
|
1526
|
+
harness,
|
|
1527
|
+
data: {
|
|
1528
|
+
capsule_id: recovery.capsule.capsule_id,
|
|
1529
|
+
generation: recovery.capsule.generation,
|
|
1530
|
+
injection_event: injectionEvent,
|
|
1531
|
+
},
|
|
1532
|
+
});
|
|
1264
1533
|
}
|
|
1265
1534
|
|
|
1266
1535
|
main()
|
|
@@ -194,6 +194,56 @@ export type ImageCaptured = EventEnvelope<
|
|
|
194
194
|
}
|
|
195
195
|
>;
|
|
196
196
|
|
|
197
|
+
// Context continuity
|
|
198
|
+
export type ContextSampled = EventEnvelope<
|
|
199
|
+
"context.sampled",
|
|
200
|
+
{
|
|
201
|
+
model?: string;
|
|
202
|
+
used_tokens?: number;
|
|
203
|
+
window_tokens?: number;
|
|
204
|
+
used_percent?: number;
|
|
205
|
+
telemetry_source: "hook" | "native_event" | "result" | "transcript" | "estimate";
|
|
206
|
+
confidence: "exact" | "reported" | "estimated";
|
|
207
|
+
}
|
|
208
|
+
>;
|
|
209
|
+
|
|
210
|
+
export type ContextCheckpointCreated = EventEnvelope<
|
|
211
|
+
"context.checkpoint.created",
|
|
212
|
+
{
|
|
213
|
+
capsule_id: string;
|
|
214
|
+
generation: number;
|
|
215
|
+
path: string;
|
|
216
|
+
reason: "manual" | "pressure" | "pre_compact" | "session_end";
|
|
217
|
+
reused: boolean;
|
|
218
|
+
}
|
|
219
|
+
>;
|
|
220
|
+
|
|
221
|
+
export type ContextCompactionStarted = EventEnvelope<
|
|
222
|
+
"context.compaction.started",
|
|
223
|
+
{
|
|
224
|
+
trigger?: string;
|
|
225
|
+
pre_tokens?: number;
|
|
226
|
+
}
|
|
227
|
+
>;
|
|
228
|
+
|
|
229
|
+
export type ContextCompactionCompleted = EventEnvelope<
|
|
230
|
+
"context.compaction.completed",
|
|
231
|
+
{
|
|
232
|
+
trigger?: string;
|
|
233
|
+
pre_tokens?: number;
|
|
234
|
+
post_tokens?: number;
|
|
235
|
+
}
|
|
236
|
+
>;
|
|
237
|
+
|
|
238
|
+
export type ContextRecoveryInjected = EventEnvelope<
|
|
239
|
+
"context.recovery.injected",
|
|
240
|
+
{
|
|
241
|
+
capsule_id: string;
|
|
242
|
+
generation: number;
|
|
243
|
+
injection_event: "SessionStart" | "UserPromptSubmit";
|
|
244
|
+
}
|
|
245
|
+
>;
|
|
246
|
+
|
|
197
247
|
// File claims
|
|
198
248
|
export type ClaimAcquire = EventEnvelope<
|
|
199
249
|
"claim.acquire",
|
|
@@ -257,6 +307,18 @@ export type StatePresenceChange = EventEnvelope<
|
|
|
257
307
|
|
|
258
308
|
export type StateHeartbeat = EventEnvelope<"state.heartbeat", Record<string, never>>;
|
|
259
309
|
|
|
310
|
+
/** A live session adopted a durable persona/role. The event is authoritative
|
|
311
|
+
* for replay; `.name-history` carries the same binding for heartbeat healing. */
|
|
312
|
+
export type IdentityAssumed = EventEnvelope<
|
|
313
|
+
"identity.assumed",
|
|
314
|
+
{
|
|
315
|
+
name: string;
|
|
316
|
+
agent_id: string;
|
|
317
|
+
previous_name?: string;
|
|
318
|
+
previous_agent_id?: string;
|
|
319
|
+
}
|
|
320
|
+
>;
|
|
321
|
+
|
|
260
322
|
// Council
|
|
261
323
|
export type CouncilOpen = EventEnvelope<
|
|
262
324
|
"council.open",
|
|
@@ -398,6 +460,11 @@ export type Event =
|
|
|
398
460
|
| ToolPostUseFailure
|
|
399
461
|
| ToolOutputChunk
|
|
400
462
|
| ImageCaptured
|
|
463
|
+
| ContextSampled
|
|
464
|
+
| ContextCheckpointCreated
|
|
465
|
+
| ContextCompactionStarted
|
|
466
|
+
| ContextCompactionCompleted
|
|
467
|
+
| ContextRecoveryInjected
|
|
401
468
|
| ClaimAcquire
|
|
402
469
|
| ClaimRelease
|
|
403
470
|
| ClaimConflict
|
|
@@ -406,6 +473,7 @@ export type Event =
|
|
|
406
473
|
| StateScratchAppend
|
|
407
474
|
| StatePresenceChange
|
|
408
475
|
| StateHeartbeat
|
|
476
|
+
| IdentityAssumed
|
|
409
477
|
| CouncilOpen
|
|
410
478
|
| CouncilRoundOpen
|
|
411
479
|
| CouncilContribution
|
|
@@ -61,6 +61,7 @@ export const CLAUDE_CODE_EVENTS: HookEvent[] = [
|
|
|
61
61
|
{ settingsKey: "PreToolUse", subcommand: "pre-tool-use" },
|
|
62
62
|
{ settingsKey: "PostToolUse", subcommand: "post-tool-use" },
|
|
63
63
|
{ settingsKey: "PostToolUseFailure", subcommand: "post-tool-use-failure" },
|
|
64
|
+
{ settingsKey: "PreCompact", subcommand: "pre-compact" },
|
|
64
65
|
];
|
|
65
66
|
|
|
66
67
|
/**
|
|
@@ -88,6 +89,8 @@ export const CODEX_EVENTS: HookEvent[] = [
|
|
|
88
89
|
{ settingsKey: "UserPromptSubmit", subcommand: "user-prompt-submit" },
|
|
89
90
|
{ settingsKey: "SubagentStart", subcommand: "sub-agent-start" },
|
|
90
91
|
{ settingsKey: "SubagentStop", subcommand: "sub-agent-stop" },
|
|
92
|
+
{ settingsKey: "PreCompact", subcommand: "pre-compact" },
|
|
93
|
+
{ settingsKey: "PostCompact", subcommand: "post-compact" },
|
|
91
94
|
{ settingsKey: "Stop", subcommand: "stop" },
|
|
92
95
|
];
|
|
93
96
|
|
|
@@ -163,6 +163,10 @@ export function normalizeEventName(
|
|
|
163
163
|
return { event_type: "tool.post_use", intra_turn: true };
|
|
164
164
|
case "post-tool-use-failure":
|
|
165
165
|
return { event_type: "tool.post_use_failure", intra_turn: true };
|
|
166
|
+
case "pre-compact":
|
|
167
|
+
return { event_type: "context.compaction.started", intra_turn: false };
|
|
168
|
+
case "post-compact":
|
|
169
|
+
return { event_type: "context.compaction.completed", intra_turn: false };
|
|
166
170
|
default:
|
|
167
171
|
return null;
|
|
168
172
|
}
|
|
@@ -177,4 +181,6 @@ export type NormalizedEventType =
|
|
|
177
181
|
| "subagent.stop"
|
|
178
182
|
| "tool.pre_use"
|
|
179
183
|
| "tool.post_use"
|
|
180
|
-
| "tool.post_use_failure"
|
|
184
|
+
| "tool.post_use_failure"
|
|
185
|
+
| "context.compaction.started"
|
|
186
|
+
| "context.compaction.completed";
|