@north-light/crouter 0.3.180 → 0.3.181
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/dist/api/client.d.ts +10 -1
- package/dist/api/client.js +13 -0
- package/dist/api/dto/broker.d.ts +32 -0
- package/dist/api/dto/crons.d.ts +17 -0
- package/dist/api/dto/memory.d.ts +17 -0
- package/dist/api/dto/memory.js +6 -0
- package/dist/api/dto/messages.d.ts +5 -0
- package/dist/api/dto/reviews.d.ts +8 -4
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +2 -0
- package/dist/api/routes.js +4 -0
- package/dist/build-root.d.ts +7 -0
- package/dist/build-root.js +21 -0
- package/dist/builtin-memory/insights/init.md +48 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/__tests__/insights-active-init.test.ts +98 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +7 -50
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +16 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/pi-shell-runner.ts +34 -0
- package/dist/cli.js +1 -2
- package/dist/clients/attach/__tests__/context-message.test.js +5 -2
- package/dist/clients/attach/assets/README.md +7 -0
- package/dist/clients/attach/assets/whip-06.mp3 +0 -0
- package/dist/clients/attach/assets/whip-crack.mp3 +0 -0
- package/dist/clients/attach/assets/whip-snap.mp3 +0 -0
- package/dist/clients/attach/chrome/canvas-panels.d.ts +7 -1
- package/dist/clients/attach/chrome/canvas-panels.js +20 -3
- package/dist/clients/attach/chrome/review-wait.d.ts +6 -0
- package/dist/clients/attach/chrome/review-wait.js +22 -0
- package/dist/clients/attach/chrome/roster.js +23 -2
- package/dist/clients/attach/chrome/widgets.js +1 -1
- package/dist/clients/attach/input/controller.js +4 -3
- package/dist/clients/attach/overlays/mcp.js +3 -1
- package/dist/clients/attach/render/chat-view.js +1 -1
- package/dist/clients/attach/session/whip.d.ts +1 -0
- package/dist/clients/attach/session/whip.js +26 -0
- package/dist/clients/attach/slash/dispatch.js +2 -0
- package/dist/clients/attach/viewer.js +578 -573
- package/dist/clients/inbox/review/document-surface.d.ts +1 -1
- package/dist/clients/inbox/review/document-surface.js +4 -4
- package/dist/clients/inbox/review/launch.js +16 -4
- package/dist/clients/inbox/review/review-client.d.ts +9 -4
- package/dist/clients/inbox/review/review-client.js +3 -0
- package/dist/commands/cron.js +30 -8
- package/dist/commands/human/prompts.d.ts +7 -2
- package/dist/commands/human/prompts.js +15 -10
- package/dist/commands/human.js +1 -2
- package/dist/commands/memory/find.js +11 -8
- package/dist/commands/memory/read.js +111 -11
- package/dist/commands/memory/write.js +1 -1
- package/dist/commands/memory.js +1 -1
- package/dist/commands/pkg/market-manage.d.ts +13 -0
- package/dist/commands/pkg/market-manage.js +39 -33
- package/dist/commands/pkg/plugin-inspect.js +4 -3
- package/dist/commands/pkg/plugin-manage.js +12 -11
- package/dist/commands/surface/node/focus.js +1 -2
- package/dist/commands/sys/doctor.js +4 -4
- package/dist/commands/sys/setup-core.d.ts +14 -7
- package/dist/commands/sys/setup-core.js +66 -11
- package/dist/commands/sys/setup-wizard.js +2 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/core/__tests__/cron-held-settlement.test.d.ts +1 -0
- package/dist/core/__tests__/cron-held-settlement.test.js +222 -0
- package/dist/core/__tests__/helpers/harness.js +1 -2
- package/dist/core/__tests__/phase4-review-store.test.js +1 -0
- package/dist/core/__tests__/serial/command-plugins.test.js +88 -1
- package/dist/core/__tests__/session-model.test.js +5 -3
- package/dist/core/bootstrap.d.ts +0 -4
- package/dist/core/bootstrap.js +1 -55
- package/dist/core/canvas/crons.d.ts +54 -2
- package/dist/core/canvas/crons.js +48 -4
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/command-manifests/manifest.d.ts +11 -0
- package/dist/core/command-manifests/manifest.js +45 -4
- package/dist/core/command-manifests/schema.d.ts +1 -1
- package/dist/core/command-plugins/bundle.d.ts +1 -0
- package/dist/core/command-plugins/bundle.js +3 -3
- package/dist/core/command-plugins/discovery.d.ts +5 -2
- package/dist/core/command-plugins/discovery.js +5 -5
- package/dist/core/command-plugins/help-addenda.d.ts +12 -0
- package/dist/core/command-plugins/help-addenda.js +30 -0
- package/dist/core/command.js +25 -2
- package/dist/core/config.js +0 -1
- package/dist/core/human/convention.d.ts +0 -1
- package/dist/core/human/convention.js +0 -6
- package/dist/core/keybindings/inbox.d.ts +6 -8
- package/dist/core/keybindings/inbox.js +6 -15
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/index.js +1 -1
- package/dist/core/memory/doc-link-grammar.js +4 -1
- package/dist/core/memory-resolver.d.ts +28 -4
- package/dist/core/memory-resolver.js +51 -39
- package/dist/core/review/stage.js +1 -0
- package/dist/core/review/store.d.ts +5 -0
- package/dist/core/review/store.js +10 -0
- package/dist/core/review/types.d.ts +4 -0
- package/dist/core/runtime/broker/event-projection.d.ts +8 -1
- package/dist/core/runtime/broker/event-projection.js +25 -1
- package/dist/core/runtime/broker/frame-dispatch.d.ts +2 -0
- package/dist/core/runtime/broker/frame-dispatch.js +50 -8
- package/dist/core/runtime/broker/message-ledger.d.ts +53 -0
- package/dist/core/runtime/broker/message-ledger.js +143 -0
- package/dist/core/runtime/broker/rebind.js +14 -0
- package/dist/core/runtime/broker-protocol.d.ts +46 -1
- package/dist/core/runtime/broker.js +11 -2
- package/dist/core/runtime/interactive-deliver.d.ts +5 -2
- package/dist/core/runtime/interactive-deliver.js +6 -3
- package/dist/core/runtime/shell-expansion.d.ts +32 -0
- package/dist/core/runtime/shell-expansion.js +102 -0
- package/dist/core/session-model/session-state.d.ts +9 -4
- package/dist/core/session-model/session-state.js +5 -1
- package/dist/daemon/api/handlers/broker-ops.js +8 -0
- package/dist/daemon/api/handlers/crons.js +14 -1
- package/dist/daemon/api/handlers/inbox.js +5 -0
- package/dist/daemon/api/handlers/memory.d.ts +2 -0
- package/dist/daemon/api/handlers/memory.js +48 -0
- package/dist/daemon/api/handlers/messages.js +7 -1
- package/dist/daemon/api/handlers/reviews.js +7 -5
- package/dist/daemon/api/map.js +3 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +71 -3
- package/dist/daemon/crtrd.js +3 -0
- package/dist/daemon/reconcilers/pending-review-submit.d.ts +7 -0
- package/dist/daemon/reconcilers/pending-review-submit.js +35 -0
- package/dist/daemon/review/companion.d.ts +8 -0
- package/dist/daemon/review/companion.js +35 -0
- package/dist/daemon/review/deliver.js +2 -1
- package/dist/daemon/review/finish.d.ts +29 -2
- package/dist/daemon/review/finish.js +75 -2
- package/dist/shared/generated-context.d.ts +3 -4
- package/dist/shared/generated-context.js +24 -6
- package/dist/types.d.ts +0 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -59,6 +59,12 @@ function parseSendBody(body) {
|
|
|
59
59
|
if (delivery !== undefined && delivery !== 'interactive') {
|
|
60
60
|
throw usage(`invalid delivery: ${String(delivery)} (expected interactive)`);
|
|
61
61
|
}
|
|
62
|
+
const messageId = b['message_id'];
|
|
63
|
+
if (messageId !== undefined && typeof messageId !== 'string') {
|
|
64
|
+
throw usage('message_id must be a string');
|
|
65
|
+
}
|
|
66
|
+
if (typeof messageId === 'string' && messageId.trim() !== '')
|
|
67
|
+
out.message_id = messageId;
|
|
62
68
|
if (delivery === 'interactive') {
|
|
63
69
|
// Interactive delivery is a live-conversation send: a plain immediate body,
|
|
64
70
|
// nothing that only makes sense on the durable path.
|
|
@@ -158,7 +164,7 @@ async function handleMessage(ctx) {
|
|
|
158
164
|
// straight there (append + revive, watcher delivers post-boot). ---
|
|
159
165
|
if (req.delivery === 'interactive' && isBrokerLive(meta)) {
|
|
160
166
|
try {
|
|
161
|
-
await deliverLive(id, req.body);
|
|
167
|
+
await deliverLive(id, req.body, req.message_id);
|
|
162
168
|
const body = {
|
|
163
169
|
node_id: id,
|
|
164
170
|
delivered: true,
|
|
@@ -172,11 +172,13 @@ async function handleSubmit(ctx) {
|
|
|
172
172
|
if (violations.length > 0 || reviewId === undefined)
|
|
173
173
|
invalidRequest(violations);
|
|
174
174
|
const finished = await submitReviewApproval(reviewId);
|
|
175
|
-
const body =
|
|
176
|
-
review: toReviewResponseDTO(finished.record),
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
const body = finished.outcome === 'awaiting_companion'
|
|
176
|
+
? { review: toReviewResponseDTO(finished.record), outcome: finished.outcome }
|
|
177
|
+
: {
|
|
178
|
+
review: toReviewResponseDTO(finished.record),
|
|
179
|
+
result: toFeedbackResultDTO(finished.result),
|
|
180
|
+
outcome: finished.outcome,
|
|
181
|
+
};
|
|
180
182
|
return { status: 200, body };
|
|
181
183
|
}
|
|
182
184
|
async function handleCancel(ctx) {
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -228,6 +228,7 @@ export function toCronDTO(c, lastRun) {
|
|
|
228
228
|
sink: sinkDisplay(c.sink),
|
|
229
229
|
tier: c.tier,
|
|
230
230
|
state: c.state,
|
|
231
|
+
held: c.held,
|
|
231
232
|
run_state: c.run_state,
|
|
232
233
|
last_run: lastRun === undefined || lastRun === null
|
|
233
234
|
? null
|
|
@@ -279,6 +280,8 @@ export function toReviewDTO(row, extras = {}) {
|
|
|
279
280
|
};
|
|
280
281
|
if (row.opened_at !== null)
|
|
281
282
|
dto.opened_at = row.opened_at;
|
|
283
|
+
if (row.submit_requested_at !== null)
|
|
284
|
+
dto.submit_requested_at = row.submit_requested_at;
|
|
282
285
|
if (row.approved_at !== null)
|
|
283
286
|
dto.approved_at = row.approved_at;
|
|
284
287
|
if (row.changed !== null)
|
|
@@ -27,6 +27,7 @@ import { focusRoutes } from './handlers/focus.js';
|
|
|
27
27
|
import { healthRoutes } from './handlers/health.js';
|
|
28
28
|
import { humanRoutes } from './handlers/human.js';
|
|
29
29
|
import { inboxRoutes } from './handlers/inbox.js';
|
|
30
|
+
import { memoryRoutes } from './handlers/memory.js';
|
|
30
31
|
import { messageRoutes } from './handlers/messages.js';
|
|
31
32
|
import { modelAuthRoutes } from './handlers/modelauth.js';
|
|
32
33
|
import { nodeRoutes } from './handlers/nodes.js';
|
|
@@ -52,6 +53,7 @@ function buildRouter() {
|
|
|
52
53
|
.registerAll(attachRoutes)
|
|
53
54
|
.registerAll(canvasRoutes)
|
|
54
55
|
.registerAll(fileRoutes)
|
|
56
|
+
.registerAll(memoryRoutes)
|
|
55
57
|
.registerAll(focusRoutes)
|
|
56
58
|
.registerAll(subscriptionRoutes)
|
|
57
59
|
.registerAll(cronRoutes)
|
package/dist/daemon/cron-run.js
CHANGED
|
@@ -95,6 +95,21 @@ const EMPTY_STDOUT_SHA256 = createHash('sha256').update('').digest('hex');
|
|
|
95
95
|
/** Lead of a `cron_runs.delivered` string written by the overlap=skip pass.
|
|
96
96
|
* Matched (not just written) — see `latestRunIsSkip`. */
|
|
97
97
|
const SKIP_MARKER = 'skipped (overlap=skip)';
|
|
98
|
+
/** EX_TEMPFAIL — the owed-gate disposition. A scheduled run exiting 75
|
|
99
|
+
* declares "this occurrence is owed but not currently eligible": the row is
|
|
100
|
+
* PARKED (held=1, occurrence not spent) instead of disposed — no failure, no
|
|
101
|
+
* escalation, no delivery, no on-change hash, no one-shot consumption. A poke
|
|
102
|
+
* (`POST /v1/crons/poke`) re-dues it now; otherwise a recurring row re-checks
|
|
103
|
+
* at its already-advanced natural slot and a held one-shot waits at its
|
|
104
|
+
* backstop (`expires_at`, else far future — poke-only). The gate itself stays
|
|
105
|
+
* in bash; the daemon only honors the exit code. */
|
|
106
|
+
const EXIT_HELD = 75;
|
|
107
|
+
/** `cron_runs.delivered` string for a held (exit-75) settlement. */
|
|
108
|
+
const HELD_DELIVERED = 'deferred (exit 75) — held for poke';
|
|
109
|
+
/** Backstop `fire_at` for a held one-shot without `--expires`: never due on
|
|
110
|
+
* the clock — the row fires only on a poke. Without this, a past-due parked
|
|
111
|
+
* one-shot would re-fire every tick forever. */
|
|
112
|
+
const HELD_ONE_SHOT_FAR_FUTURE = '9999-12-31T23:59:59.999Z';
|
|
98
113
|
/** One id per daemon process instance — distinguishes THIS process's own
|
|
99
114
|
* in-flight run leases from a stale 'running' row a prior (crashed/restarted)
|
|
100
115
|
* daemon left behind. */
|
|
@@ -451,7 +466,9 @@ export function recoverStaleCronLeases(now, options = {}) {
|
|
|
451
466
|
stderr_head: ctx.stderrHead,
|
|
452
467
|
delivered: disposition.delivered,
|
|
453
468
|
});
|
|
454
|
-
|
|
469
|
+
// A recovered-timeout settlement took the ordinary disposition path
|
|
470
|
+
// (pause + escalation unless silent) — the gate is resolved, clear held.
|
|
471
|
+
releaseCronRunLease(c.cron_id, { kind: 'clear' });
|
|
455
472
|
if (c.recur == null && !disposition.paused)
|
|
456
473
|
consumeCron(c.cron_id);
|
|
457
474
|
})();
|
|
@@ -720,6 +737,16 @@ export function executeCron(c, opts) {
|
|
|
720
737
|
return;
|
|
721
738
|
}
|
|
722
739
|
const finishedAtMs = Date.now();
|
|
740
|
+
// The owed-gate branch (exit 75): a peer of the wasReplaced
|
|
741
|
+
// short-circuit — skip disposition entirely (no delivery, no
|
|
742
|
+
// escalation, no pause, no on-change hash, no one-shot consumption)
|
|
743
|
+
// and settle the row's held state with the lease release instead. The
|
|
744
|
+
// exclusions are deliberate: a manual `cron run` must not change
|
|
745
|
+
// scheduling state; a timeout-killed or launch-failed run is a real
|
|
746
|
+
// failure even if a stray 75 surfaces; a replaced or self-canceled
|
|
747
|
+
// run's existing short-circuits win.
|
|
748
|
+
const heldDeferred = !outOfBand && !timedOut && processError == null && code === EXIT_HELD && !wasReplaced && !wasSelfCanceled;
|
|
749
|
+
let heldSettle;
|
|
723
750
|
let delivered;
|
|
724
751
|
let paused = false;
|
|
725
752
|
if (wasReplaced) {
|
|
@@ -727,6 +754,38 @@ export function executeCron(c, opts) {
|
|
|
727
754
|
// fresh: record it, no disposition, no escalation.
|
|
728
755
|
delivered = 'replaced (overlap=replace)';
|
|
729
756
|
}
|
|
757
|
+
else if (heldDeferred) {
|
|
758
|
+
delivered = HELD_DELIVERED;
|
|
759
|
+
// Re-read the row: `last_poke_at` must be CURRENT — a poke landing
|
|
760
|
+
// while this gate check was in flight is exactly the race this
|
|
761
|
+
// check closes. (A row canceled mid-run is gone; recordCronRun and
|
|
762
|
+
// the lease release below both no-op.)
|
|
763
|
+
const fresh = getCron(c.cron_id);
|
|
764
|
+
if (fresh !== null) {
|
|
765
|
+
if (fresh.state === 'active' && fresh.last_poke_at !== null && startedAtIso < fresh.last_poke_at) {
|
|
766
|
+
// A poke arrived mid-run — its unpark statement deliberately
|
|
767
|
+
// skips leased rows — so re-due instead of park: the gate
|
|
768
|
+
// re-checks next tick instead of stalling until the backstop
|
|
769
|
+
// while the very eligibility it waits for is present. Only for
|
|
770
|
+
// a row still ACTIVE: a row paused mid-run parks below instead —
|
|
771
|
+
// the poke skips paused rows on purpose while its stamp lands on
|
|
772
|
+
// every row, and a re-due would leave a past-due UNHELD row that
|
|
773
|
+
// `cron resume` fires blind even though its gate never passed.
|
|
774
|
+
heldSettle = { kind: 'redue', nowIso: new Date(finishedAtMs).toISOString() };
|
|
775
|
+
}
|
|
776
|
+
else if (c.recur != null) {
|
|
777
|
+
// Recurring: the pre-run advance already wrote the natural next
|
|
778
|
+
// slot, which IS the backstop — park and touch nothing else.
|
|
779
|
+
heldSettle = { kind: 'park' };
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
// One-shot: park at its backstop. With --expires, expiry deletes
|
|
783
|
+
// the row unfired at that instant (bounded wait); without, the
|
|
784
|
+
// far-future fire_at means poke-only.
|
|
785
|
+
heldSettle = { kind: 'park', fireAt: fresh.expires_at ?? HELD_ONE_SHOT_FAR_FUTURE };
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
730
789
|
else {
|
|
731
790
|
const disposition = await disposeSettledRun(c, {
|
|
732
791
|
runId,
|
|
@@ -758,7 +817,13 @@ export function executeCron(c, opts) {
|
|
|
758
817
|
delivered,
|
|
759
818
|
};
|
|
760
819
|
recordCronRun(record);
|
|
761
|
-
if (
|
|
820
|
+
if (heldDeferred) {
|
|
821
|
+
// Held: the occurrence is NOT spent — even a one-shot is retained
|
|
822
|
+
// (like a paused escalated one-shot is). Park/re-due and the lease
|
|
823
|
+
// release are one write.
|
|
824
|
+
releaseCronRunLease(c.cron_id, heldSettle);
|
|
825
|
+
}
|
|
826
|
+
else if (!outOfBand && c.recur == null && !paused) {
|
|
762
827
|
// One-shot: the run is done, delete the row (its history cascades
|
|
763
828
|
// with it — matching one-shot consumption). A failing
|
|
764
829
|
// one-shot that PAUSED for escalation is kept, so `cron resume` can
|
|
@@ -767,7 +832,10 @@ export function executeCron(c, opts) {
|
|
|
767
832
|
consumeCron(c.cron_id);
|
|
768
833
|
}
|
|
769
834
|
else {
|
|
770
|
-
|
|
835
|
+
// An ordinary scheduled disposition settlement resolves the gate,
|
|
836
|
+
// so it unparks a held row; a replaced or out-of-band run leaves
|
|
837
|
+
// `held` untouched (the gate is still unresolved).
|
|
838
|
+
releaseCronRunLease(c.cron_id, wasReplaced || outOfBand ? undefined : { kind: 'clear' });
|
|
771
839
|
}
|
|
772
840
|
resolve(record);
|
|
773
841
|
})().catch(reject);
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -57,6 +57,7 @@ import { DEFAULT_INTERVAL_MS } from './supervise-cadence.js';
|
|
|
57
57
|
import { BrokerSupervisionReconciler } from './reconcilers/broker-supervision.js';
|
|
58
58
|
import { ControllerDeathReconciler, DormantInboxReconciler } from './reconcilers/dormant-inbox.js';
|
|
59
59
|
import { CronLaneReconciler } from './reconcilers/cron-lane.js';
|
|
60
|
+
import { PendingReviewSubmitReconciler } from './reconcilers/pending-review-submit.js';
|
|
60
61
|
import { StorageMaintenanceReconciler } from './reconcilers/storage-maintenance.js';
|
|
61
62
|
import { captureLivenessSnapshot, capturePidCommand, captureTeardownSnapshot, isPidAlive, killProcessTreePids, } from '../core/canvas/pid.js';
|
|
62
63
|
import { bindFleet, boundFleet } from '../core/runtime/fleet.js';
|
|
@@ -311,6 +312,7 @@ const brokerSupervision = new BrokerSupervisionReconciler();
|
|
|
311
312
|
const controllerDeath = new ControllerDeathReconciler();
|
|
312
313
|
const dormantInbox = new DormantInboxReconciler();
|
|
313
314
|
const cronLane = new CronLaneReconciler();
|
|
315
|
+
const pendingReviewSubmit = new PendingReviewSubmitReconciler();
|
|
314
316
|
const storageMaintenance = new StorageMaintenanceReconciler();
|
|
315
317
|
/** Supervision tick over the daemon's current live set. */
|
|
316
318
|
export async function superviseTick(now = Date.now(), lifecycle = directTickLifecycle) {
|
|
@@ -334,6 +336,7 @@ export async function superviseTick(now = Date.now(), lifecycle = directTickLife
|
|
|
334
336
|
controllerDeath.run(now, { rows });
|
|
335
337
|
dormantInbox.run(now, { rows, fleet, hasBrokerCapacity });
|
|
336
338
|
cronLane.run(now, { lifecycle, hasBrokerCapacity });
|
|
339
|
+
pendingReviewSubmit.run({ lifecycle });
|
|
337
340
|
storageMaintenance.run(now);
|
|
338
341
|
}
|
|
339
342
|
/** Start the supervisor loop after winning the authoritative canvas claim.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The safety net under a deferred review submit.
|
|
2
|
+
//
|
|
3
|
+
// The companion's turn end is what normally completes a submit the human left
|
|
4
|
+
// waiting (`daemon/api/handlers/broker-ops.ts`). A companion that crashed, was
|
|
5
|
+
// torn down, or ended its turn while this daemon was starting never delivers
|
|
6
|
+
// that settlement, so this pass completes any review whose companion is no
|
|
7
|
+
// longer working. Without it a review could wait forever on a turn end that is
|
|
8
|
+
// never coming.
|
|
9
|
+
import { emitEvent } from '../../core/events/emit.js';
|
|
10
|
+
import { operationIdContext } from '../../core/events/operation-id.js';
|
|
11
|
+
import { listSubmitRequestedReviews } from '../../core/review/store.js';
|
|
12
|
+
import { completeRequestedSubmit } from '../review/finish.js';
|
|
13
|
+
export class PendingReviewSubmitReconciler {
|
|
14
|
+
run(ctx) {
|
|
15
|
+
if (!ctx.lifecycle.acceptsDetachedWork())
|
|
16
|
+
return;
|
|
17
|
+
let pending;
|
|
18
|
+
try {
|
|
19
|
+
pending = listSubmitRequestedReviews();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
operationIdContext.fresh(() => {
|
|
23
|
+
emitEvent({ level: 'error', event: 'review.deferred_submit.scan_failed', error });
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const review of pending) {
|
|
28
|
+
ctx.lifecycle.registerDetached(completeRequestedSubmit(review.review_id, { atCompanionTurnEnd: false }).then(() => { }, (error) => {
|
|
29
|
+
operationIdContext.fresh(() => {
|
|
30
|
+
emitEvent({ level: 'error', event: 'review.deferred_submit.failed', error });
|
|
31
|
+
});
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Is the companion inside a turn right now? */
|
|
2
|
+
export declare function companionMidTurn(nodeId: string): boolean;
|
|
3
|
+
/** Does the companion hold work it has not yet consumed? A deferred entry is
|
|
4
|
+
* excluded for the same reason the settlement policy excludes it: it never
|
|
5
|
+
* starts a turn of its own. */
|
|
6
|
+
export declare function companionHasQueuedWork(nodeId: string): boolean;
|
|
7
|
+
/** Would submitting right now step on the companion's work? */
|
|
8
|
+
export declare function companionStillWorking(nodeId: string): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Is a review companion still working? This is what a deferred submit waits on.
|
|
2
|
+
//
|
|
3
|
+
// Two facts, both already maintained by the runtime, and neither one enough
|
|
4
|
+
// alone:
|
|
5
|
+
//
|
|
6
|
+
// - MID-TURN — the `<jobDir>/busy` marker AND-ed with a live broker pid, the
|
|
7
|
+
// canonical "pi is inside a turn" signal (`core/runtime/busy.ts`). A stale
|
|
8
|
+
// marker from a crashed broker reads false through the AND.
|
|
9
|
+
// - QUEUED — an undelivered non-deferred inbox entry. Comments are pushed to
|
|
10
|
+
// the companion as inbox messages, so the ordinary rhythm is: the human
|
|
11
|
+
// comments, the entry lands, the companion's turn starts a moment later.
|
|
12
|
+
// Between those two moments the busy marker is cold while the companion is
|
|
13
|
+
// unmistakably about to work, and submitting into that gap kills the edit
|
|
14
|
+
// the wait exists to protect.
|
|
15
|
+
import { getNode } from '../../core/canvas/canvas.js';
|
|
16
|
+
import { isPidAlive } from '../../core/canvas/pid.js';
|
|
17
|
+
import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
18
|
+
import { isBusy } from '../../core/runtime/busy.js';
|
|
19
|
+
/** Is the companion inside a turn right now? */
|
|
20
|
+
export function companionMidTurn(nodeId) {
|
|
21
|
+
const node = getNode(nodeId);
|
|
22
|
+
if (node === null || !isPidAlive(node.pi_pid))
|
|
23
|
+
return false;
|
|
24
|
+
return isBusy(nodeId);
|
|
25
|
+
}
|
|
26
|
+
/** Does the companion hold work it has not yet consumed? A deferred entry is
|
|
27
|
+
* excluded for the same reason the settlement policy excludes it: it never
|
|
28
|
+
* starts a turn of its own. */
|
|
29
|
+
export function companionHasQueuedWork(nodeId) {
|
|
30
|
+
return readInboxSince(nodeId, readCursor(nodeId)).some((entry) => entry.tier !== 'deferred');
|
|
31
|
+
}
|
|
32
|
+
/** Would submitting right now step on the companion's work? */
|
|
33
|
+
export function companionStillWorking(nodeId) {
|
|
34
|
+
return companionMidTurn(nodeId) || companionHasQueuedWork(nodeId);
|
|
35
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FinalizationError, pushFinal, redeliverFinalReport } from '../../core/feed/feed.js';
|
|
2
|
+
import { formatReviewApproval } from '../../shared/generated-context.js';
|
|
2
3
|
import { deliverNodeMessage } from '../messaging/node-message.js';
|
|
3
4
|
function approvalBody(review, resultPath) {
|
|
4
5
|
const summary = review.changed
|
|
@@ -8,7 +9,7 @@ function approvalBody(review, resultPath) {
|
|
|
8
9
|
}
|
|
9
10
|
/** Render the immutable approval fact without replaying comment text. */
|
|
10
11
|
export function renderApprovalMessage(review, resultPath) {
|
|
11
|
-
return approvalBody(review, resultPath);
|
|
12
|
+
return formatReviewApproval(approvalBody(review, resultPath));
|
|
12
13
|
}
|
|
13
14
|
function recordedFailedFanout(review, reportBasename) {
|
|
14
15
|
const error = review.delivery_error;
|
|
@@ -5,6 +5,13 @@ export interface ReviewApprovalFinishOutcome {
|
|
|
5
5
|
record: ReviewRecord;
|
|
6
6
|
result: ReviewResult;
|
|
7
7
|
}
|
|
8
|
+
/** The human submitted while the companion was still working. The review stays
|
|
9
|
+
* open and the daemon completes the approval once the companion goes quiet. */
|
|
10
|
+
export interface ReviewSubmitDeferredOutcome {
|
|
11
|
+
outcome: 'awaiting_companion';
|
|
12
|
+
record: ReviewRecord;
|
|
13
|
+
}
|
|
14
|
+
export type ReviewSubmitOutcome = ReviewApprovalFinishOutcome | ReviewSubmitDeferredOutcome;
|
|
8
15
|
export interface ReviewCancellationFinishOutcome {
|
|
9
16
|
outcome: 'settled' | 'already_settled';
|
|
10
17
|
record: ReviewRecord;
|
|
@@ -13,8 +20,28 @@ export interface ReviewCancellationFinishOutcome {
|
|
|
13
20
|
export type ReviewFinishOutcome = ReviewApprovalFinishOutcome | ReviewCancellationFinishOutcome;
|
|
14
21
|
/** Retire a companion only through a lifecycle transition legal for its current status. */
|
|
15
22
|
export declare function retireCompanion(nodeId: string, outcome: 'approved' | 'canceled'): 'retired' | 'already' | 'missing';
|
|
16
|
-
/**
|
|
17
|
-
|
|
23
|
+
/** Approve on the human's behalf, waiting first when the companion is still
|
|
24
|
+
* working. Waiting is the only new outcome: an idle companion approves in the
|
|
25
|
+
* same pass it always did, and the wait never freezes the review — comments
|
|
26
|
+
* added during it are in the snapshot, because the snapshot happens at
|
|
27
|
+
* completion and the review is still open. */
|
|
28
|
+
export declare function submitReviewApproval(reviewId: string): Promise<ReviewSubmitOutcome>;
|
|
29
|
+
/** Complete a submit the human already pressed, once its companion is quiet.
|
|
30
|
+
* Returns null when there is nothing to complete — no pending intent, the
|
|
31
|
+
* review moved on, or the companion is still working. Idempotent: concurrent
|
|
32
|
+
* callers race on the same conditional `state = 'open'` approval write, so
|
|
33
|
+
* only one can win and the loser reports what the winner committed.
|
|
34
|
+
*
|
|
35
|
+
* `atCompanionTurnEnd` marks the one caller that IS the companion's turn end.
|
|
36
|
+
* There the mid-turn marker may not be cleared yet, so it is not consulted;
|
|
37
|
+
* queued work still defers, because an inbox entry means another turn is
|
|
38
|
+
* coming and the wait should outlast it. */
|
|
39
|
+
export declare function completeRequestedSubmit(reviewId: string, opts: {
|
|
40
|
+
atCompanionTurnEnd: boolean;
|
|
41
|
+
}): Promise<ReviewApprovalFinishOutcome | null>;
|
|
42
|
+
/** The companion's turn ended: complete any submit the human left waiting on
|
|
43
|
+
* it. Never throws — a settlement directive must not depend on delivery. */
|
|
44
|
+
export declare function completeCompanionRequestedSubmit(companionNodeId: string): Promise<void>;
|
|
18
45
|
/** Cancel an open review once; ticket cancellation remains owned by the retained human-ticket finisher. */
|
|
19
46
|
export declare function cancelReview(reviewId: string, args: {
|
|
20
47
|
reason?: string;
|
|
@@ -8,11 +8,14 @@ import { reviewResultPath } from '../../core/canvas/paths.js';
|
|
|
8
8
|
import { snapshotCommentsLocked } from '../../core/review/comments.js';
|
|
9
9
|
import { readReviewSource } from '../../core/review/document.js';
|
|
10
10
|
import { signalReviewActivity } from '../../core/review/signal.js';
|
|
11
|
-
import { approveReviewLocked, cancelReviewRow, markDelivered, markDeliveryError, markProjected, markProjectionError, requireVisibleReview, } from '../../core/review/store.js';
|
|
11
|
+
import { approveReviewLocked, cancelReviewRow, getReview, getReviewByCompanion, markDelivered, markDeliveryError, markProjected, markProjectionError, requestReviewSubmit, requireVisibleReview, } from '../../core/review/store.js';
|
|
12
12
|
import { ReviewOperationError } from '../../core/review/types.js';
|
|
13
13
|
import { transition } from '../../core/runtime/lifecycle.js';
|
|
14
14
|
import { headlessBrokerHost } from '../../core/runtime/host.js';
|
|
15
|
+
import { emitEvent } from '../../core/events/emit.js';
|
|
16
|
+
import { deliverNodeMessage } from '../messaging/node-message.js';
|
|
15
17
|
import { cancelHumanTicket, deliverTerminalResult } from '../human/finish.js';
|
|
18
|
+
import { companionHasQueuedWork, companionMidTurn, companionStillWorking } from './companion.js';
|
|
16
19
|
import { deliverApproval } from './deliver.js';
|
|
17
20
|
/** One daemon serializes terminal side effects for each immutable review row. */
|
|
18
21
|
const finishChains = new Map();
|
|
@@ -178,9 +181,46 @@ async function finishCanceled(review, winningWrite, args) {
|
|
|
178
181
|
retireCompanion(current.companion_node_id, 'canceled');
|
|
179
182
|
return { record: requireVisibleReview(review.review_id), ...(canceledTicket === undefined ? {} : { ticket_result: canceledTicket }) };
|
|
180
183
|
}
|
|
181
|
-
/**
|
|
184
|
+
/** Tell the origin its review was submitted but is finishing behind the
|
|
185
|
+
* companion. Deliberately deferred: the origin is told nothing is needed of
|
|
186
|
+
* it, so waking it to say so would be noise. The approval itself wakes it. */
|
|
187
|
+
async function noteSubmitQueued(review) {
|
|
188
|
+
try {
|
|
189
|
+
await deliverNodeMessage({
|
|
190
|
+
node_id: review.origin_node_id,
|
|
191
|
+
from: 'crtrd',
|
|
192
|
+
mode: 'quiet',
|
|
193
|
+
label: 'human review submitted — result to follow',
|
|
194
|
+
data: { review_id: review.review_id },
|
|
195
|
+
body: `The human submitted their review of \`${review.file}\`; the document is still being edited. `
|
|
196
|
+
+ "You'll be notified with the result once the review completes. Nothing is needed from you now.",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
emitEvent({ level: 'warn', event: 'review.submit_queued_note.failed', error });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Approve on the human's behalf, waiting first when the companion is still
|
|
204
|
+
* working. Waiting is the only new outcome: an idle companion approves in the
|
|
205
|
+
* same pass it always did, and the wait never freezes the review — comments
|
|
206
|
+
* added during it are in the snapshot, because the snapshot happens at
|
|
207
|
+
* completion and the review is still open. */
|
|
182
208
|
export async function submitReviewApproval(reviewId) {
|
|
183
209
|
const initial = requireVisibleReview(reviewId);
|
|
210
|
+
if (initial.state === 'canceled')
|
|
211
|
+
return terminalConflict(initial, 'submit');
|
|
212
|
+
if (initial.state === 'open' && companionStillWorking(initial.companion_node_id)) {
|
|
213
|
+
if (requestReviewSubmit(reviewId, now())) {
|
|
214
|
+
signalReviewActivity();
|
|
215
|
+
await noteSubmitQueued(initial);
|
|
216
|
+
}
|
|
217
|
+
return { outcome: 'awaiting_companion', record: requireVisibleReview(reviewId) };
|
|
218
|
+
}
|
|
219
|
+
return await approveReview(initial);
|
|
220
|
+
}
|
|
221
|
+
/** Commit approval once, then independently finish its projection, delivery, and companion retirement ledgers. */
|
|
222
|
+
async function approveReview(initial) {
|
|
223
|
+
const reviewId = initial.review_id;
|
|
184
224
|
if (initial.state === 'canceled')
|
|
185
225
|
return terminalConflict(initial, 'submit');
|
|
186
226
|
if (initial.state === 'approved') {
|
|
@@ -219,6 +259,39 @@ export async function submitReviewApproval(reviewId) {
|
|
|
219
259
|
const record = await serializedFinish(reviewId, () => finishApproved(committed.review, committed.result));
|
|
220
260
|
return { outcome: committed.outcome, record, result: committed.result };
|
|
221
261
|
}
|
|
262
|
+
/** Complete a submit the human already pressed, once its companion is quiet.
|
|
263
|
+
* Returns null when there is nothing to complete — no pending intent, the
|
|
264
|
+
* review moved on, or the companion is still working. Idempotent: concurrent
|
|
265
|
+
* callers race on the same conditional `state = 'open'` approval write, so
|
|
266
|
+
* only one can win and the loser reports what the winner committed.
|
|
267
|
+
*
|
|
268
|
+
* `atCompanionTurnEnd` marks the one caller that IS the companion's turn end.
|
|
269
|
+
* There the mid-turn marker may not be cleared yet, so it is not consulted;
|
|
270
|
+
* queued work still defers, because an inbox entry means another turn is
|
|
271
|
+
* coming and the wait should outlast it. */
|
|
272
|
+
export async function completeRequestedSubmit(reviewId, opts) {
|
|
273
|
+
const review = getReview(reviewId);
|
|
274
|
+
if (review === null || review.state !== 'open' || review.submit_requested_at === null)
|
|
275
|
+
return null;
|
|
276
|
+
if (companionHasQueuedWork(review.companion_node_id))
|
|
277
|
+
return null;
|
|
278
|
+
if (!opts.atCompanionTurnEnd && companionMidTurn(review.companion_node_id))
|
|
279
|
+
return null;
|
|
280
|
+
return await approveReview(review);
|
|
281
|
+
}
|
|
282
|
+
/** The companion's turn ended: complete any submit the human left waiting on
|
|
283
|
+
* it. Never throws — a settlement directive must not depend on delivery. */
|
|
284
|
+
export async function completeCompanionRequestedSubmit(companionNodeId) {
|
|
285
|
+
try {
|
|
286
|
+
const review = getReviewByCompanion(companionNodeId);
|
|
287
|
+
if (review === null || review.state !== 'open' || review.submit_requested_at === null)
|
|
288
|
+
return;
|
|
289
|
+
await completeRequestedSubmit(review.review_id, { atCompanionTurnEnd: true });
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
emitEvent({ level: 'error', event: 'review.deferred_submit.failed', error });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
222
295
|
/** Cancel an open review once; ticket cancellation remains owned by the retained human-ticket finisher. */
|
|
223
296
|
export async function cancelReview(reviewId, args) {
|
|
224
297
|
const initial = requireVisibleReview(reviewId);
|
|
@@ -20,6 +20,8 @@ export declare const CONNECTION_FAULT_RECOVERY_BODY = "The network connection is
|
|
|
20
20
|
export declare const PROVIDER_FAULT_RECOVERY_BODY = "Your previous turn stopped on a provider fault. Continue from where you left off and retry the work that failed.";
|
|
21
21
|
/** Mark an active persona transition without changing the guidance inside it. */
|
|
22
22
|
export declare function formatPersonaTransition(guidance: string): string;
|
|
23
|
+
/** Mark a daemon-delivered review approval for compact viewer presentation. */
|
|
24
|
+
export declare function formatReviewApproval(body: string): string;
|
|
23
25
|
/** Format the stop guard's dynamic structured-output mandate. */
|
|
24
26
|
export declare function formatStructuredOutputReprompt(schema: string): string;
|
|
25
27
|
export type ModelFallbackRecoveryReason = 'credential' | 'not-found';
|
|
@@ -31,15 +33,12 @@ export interface GeneratedContextMessageLike {
|
|
|
31
33
|
content?: unknown;
|
|
32
34
|
}
|
|
33
35
|
export interface GeneratedContextPresentation {
|
|
34
|
-
kind: 'bearings' | 'situational' | 'revive' | 'context-nudge' | 'inbox' | 'persona-transition' | 'stop-guard' | 'recovery';
|
|
36
|
+
kind: 'bearings' | 'situational' | 'revive' | 'context-nudge' | 'inbox' | 'review-approval' | 'persona-transition' | 'stop-guard' | 'recovery';
|
|
35
37
|
label: string;
|
|
36
38
|
summary: string;
|
|
37
39
|
/** The unmodified, model-visible message content. */
|
|
38
40
|
body: string;
|
|
39
41
|
}
|
|
40
|
-
/** The display-only disclosure body for a generated context card. The full
|
|
41
|
-
* message remains in `body` for the model and transcript bookkeeping; the two
|
|
42
|
-
* prompt-sized cards expose the one piece a person needs to inspect. */
|
|
43
42
|
export declare function generatedContextExpandedBody(presentation: GeneratedContextPresentation): string;
|
|
44
43
|
/** Plain text from a generated message's string or text-block content. */
|
|
45
44
|
export declare function generatedContextText(message: GeneratedContextMessageLike): string;
|
|
@@ -31,6 +31,8 @@ export const CONNECTION_FAULT_RECOVERY_BODY = 'The network connection is back on
|
|
|
31
31
|
export const PROVIDER_FAULT_RECOVERY_BODY = 'Your previous turn stopped on a provider fault. Continue from where you left off and retry the work that failed.';
|
|
32
32
|
const PERSONA_TRANSITION_OPEN = '<persona-transition>';
|
|
33
33
|
const PERSONA_TRANSITION_CLOSE = '</persona-transition>';
|
|
34
|
+
const REVIEW_APPROVAL_OPEN = '<crtr-review-approval>';
|
|
35
|
+
const REVIEW_APPROVAL_CLOSE = '</crtr-review-approval>';
|
|
34
36
|
const MODEL_FALLBACK_RECOVERY_OPEN = '<model-fallback-recovery>';
|
|
35
37
|
const MODEL_FALLBACK_RECOVERY_CLOSE = '</model-fallback-recovery>';
|
|
36
38
|
const STRUCTURED_OUTPUT_REPROMPT_PREFIX = 'You must call the `submit` tool with a result matching the required schema before you can stop. You cannot finish or go dormant any other way while this request is pending.\n\nRequired schema:\n\n```json\n';
|
|
@@ -39,6 +41,10 @@ const STRUCTURED_OUTPUT_REPROMPT_SUFFIX = '\n```';
|
|
|
39
41
|
export function formatPersonaTransition(guidance) {
|
|
40
42
|
return `${PERSONA_TRANSITION_OPEN}\n${guidance}\n${PERSONA_TRANSITION_CLOSE}`;
|
|
41
43
|
}
|
|
44
|
+
/** Mark a daemon-delivered review approval for compact viewer presentation. */
|
|
45
|
+
export function formatReviewApproval(body) {
|
|
46
|
+
return `${REVIEW_APPROVAL_OPEN}\n${body}\n${REVIEW_APPROVAL_CLOSE}`;
|
|
47
|
+
}
|
|
42
48
|
/** Format the stop guard's dynamic structured-output mandate. */
|
|
43
49
|
export function formatStructuredOutputReprompt(schema) {
|
|
44
50
|
return `${STRUCTURED_OUTPUT_REPROMPT_PREFIX}${schema}${STRUCTURED_OUTPUT_REPROMPT_SUFFIX}`;
|
|
@@ -55,14 +61,18 @@ const YIELD_MESSAGE_BLOCK = /<yield-message>([\s\S]*?)<\/yield-message>/;
|
|
|
55
61
|
/** The display-only disclosure body for a generated context card. The full
|
|
56
62
|
* message remains in `body` for the model and transcript bookkeeping; the two
|
|
57
63
|
* prompt-sized cards expose the one piece a person needs to inspect. */
|
|
64
|
+
function unmarkReviewApprovals(body) {
|
|
65
|
+
return body.replaceAll(`${REVIEW_APPROVAL_OPEN}\n`, '').replaceAll(`\n${REVIEW_APPROVAL_CLOSE}`, '');
|
|
66
|
+
}
|
|
58
67
|
export function generatedContextExpandedBody(presentation) {
|
|
68
|
+
let body = presentation.body;
|
|
59
69
|
if (presentation.label === 'crtr context') {
|
|
60
|
-
|
|
70
|
+
body = AUTO_LOADED_CONTEXT_BLOCK.exec(body)?.[1]?.trim() ?? '';
|
|
61
71
|
}
|
|
62
|
-
if (presentation.kind === 'revive') {
|
|
63
|
-
|
|
72
|
+
else if (presentation.kind === 'revive') {
|
|
73
|
+
body = YIELD_MESSAGE_BLOCK.exec(body)?.[1]?.trim() ?? '';
|
|
64
74
|
}
|
|
65
|
-
return
|
|
75
|
+
return unmarkReviewApprovals(body);
|
|
66
76
|
}
|
|
67
77
|
/** Plain text from a generated message's string or text-block content. */
|
|
68
78
|
export function generatedContextText(message) {
|
|
@@ -88,7 +98,9 @@ function nudgeSummary(body) {
|
|
|
88
98
|
}
|
|
89
99
|
// Stable header emitted by core/feed/inbox.ts coalesce(). Human-authored inbox
|
|
90
100
|
// messages are deliberately delivered verbatim and never carry this header.
|
|
91
|
-
|
|
101
|
+
// Senders are node ids, the two reserved words, or the daemon itself (crtrd —
|
|
102
|
+
// review notes and other daemon-originated node messages).
|
|
103
|
+
const INBOX_HEADER_RE = /^From ([a-z0-9]+(?:-[a-z0-9]+)+|system|human|crtrd) — (\d+) update/m;
|
|
92
104
|
export function isInboxDigest(text) {
|
|
93
105
|
return INBOX_HEADER_RE.test(text);
|
|
94
106
|
}
|
|
@@ -96,7 +108,7 @@ export function extractInboxSender(text) {
|
|
|
96
108
|
return INBOX_HEADER_RE.exec(text)?.[1] ?? null;
|
|
97
109
|
}
|
|
98
110
|
function inboxSummary(body) {
|
|
99
|
-
const headers = [...body.matchAll(/^From ([a-z0-9]+(?:-[a-z0-9]+)+|system|human) — (\d+) update/gm)];
|
|
111
|
+
const headers = [...body.matchAll(/^From ([a-z0-9]+(?:-[a-z0-9]+)+|system|human|crtrd) — (\d+) update/gm)];
|
|
100
112
|
if (headers.length === 0)
|
|
101
113
|
return 'node update';
|
|
102
114
|
const updates = headers.reduce((total, match) => total + Number(match[2] ?? 0), 0);
|
|
@@ -109,6 +121,9 @@ function inboxSummary(body) {
|
|
|
109
121
|
function isPersonaTransition(body) {
|
|
110
122
|
return body.startsWith(`${PERSONA_TRANSITION_OPEN}\n`) && body.endsWith(`\n${PERSONA_TRANSITION_CLOSE}`);
|
|
111
123
|
}
|
|
124
|
+
function isReviewApproval(body) {
|
|
125
|
+
return body.startsWith(`${REVIEW_APPROVAL_OPEN}\n`) && body.endsWith(`\n${REVIEW_APPROVAL_CLOSE}`);
|
|
126
|
+
}
|
|
112
127
|
function isStructuredOutputReprompt(body) {
|
|
113
128
|
if (!body.startsWith(STRUCTURED_OUTPUT_REPROMPT_PREFIX) || !body.endsWith(STRUCTURED_OUTPUT_REPROMPT_SUFFIX)) {
|
|
114
129
|
return false;
|
|
@@ -189,6 +204,9 @@ export function generatedContextPresentation(message) {
|
|
|
189
204
|
if (isInboxDigest(body)) {
|
|
190
205
|
return { kind: 'inbox', label: 'crtr inbox', summary: inboxSummary(body), body };
|
|
191
206
|
}
|
|
207
|
+
if (isReviewApproval(body)) {
|
|
208
|
+
return { kind: 'review-approval', label: 'crtr review', summary: 'human review approved', body };
|
|
209
|
+
}
|
|
192
210
|
if (isPersonaTransition(body)) {
|
|
193
211
|
return {
|
|
194
212
|
kind: 'persona-transition',
|
package/dist/types.d.ts
CHANGED
|
@@ -263,7 +263,6 @@ export interface ScopeState {
|
|
|
263
263
|
last_updated?: string;
|
|
264
264
|
}>;
|
|
265
265
|
last_self_check?: string;
|
|
266
|
-
bootstrap_done?: boolean;
|
|
267
266
|
/** The name of the remote canvas target `crtr canvas use` last selected
|
|
268
267
|
* (see `RemoteCanvasConfig`), or `null`/omitted for local. Read by
|
|
269
268
|
* `resolveCanvasSource` (`core/canvas/source.ts`) as the durable fallback
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.181",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.181",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|