instar 1.3.370 → 1.3.372

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.
@@ -4997,12 +4997,40 @@ export function createRoutes(ctx) {
4997
4997
  // Read-only cross-topic index: every topic with intent state + its current focus +
4998
4998
  // high-specificity tags + whether a session is live on it. Signal-only; never gates.
4999
4999
  // 503 when the index is unavailable (no stateDir / init failed).
5000
- router.get('/parallel-work/activities', (_req, res) => {
5000
+ router.get('/parallel-work/activities', async (req, res) => {
5001
5001
  if (!ctx.parallelActivityIndex) {
5002
5002
  res.status(503).json({ error: 'parallel-activity index unavailable (no stateDir or init failed)' });
5003
5003
  return;
5004
5004
  }
5005
5005
  const activities = ctx.parallelActivityIndex.activities();
5006
+ // P4 (POOL-WIDE-PARALLEL-WORK-SPEC §3): ?scope=pool composes the local
5007
+ // rows with replica-derived remote rows (LOCAL replica files — no peer
5008
+ // fan-out; an offline peer's last-replicated streams still answer).
5009
+ // Default scope stays byte-identical; a dark replica layer degrades the
5010
+ // pool scope to local rows only (200, never a new 503).
5011
+ if (req.query.scope === 'pool') {
5012
+ try {
5013
+ const poolMod = await import('../core/PoolActivityView.js');
5014
+ const readerMod = await import('../core/CoherenceJournalReader.js');
5015
+ const view = poolMod.buildPoolActivityView({
5016
+ ownMachineId: ctx.meshSelfId ?? 'local',
5017
+ local: activities,
5018
+ reader: new readerMod.CoherenceJournalReader({ stateDir: ctx.config.stateDir }),
5019
+ });
5020
+ res.json({
5021
+ scope: 'pool',
5022
+ count: view.rows.length,
5023
+ runningCount: view.rows.filter((a) => a.running).length,
5024
+ activities: view.rows,
5025
+ pool: view.pool,
5026
+ });
5027
+ return;
5028
+ }
5029
+ catch (err) {
5030
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
5031
+ return;
5032
+ }
5033
+ }
5006
5034
  res.json({
5007
5035
  count: activities.length,
5008
5036
  runningCount: activities.filter((a) => a.running).length,
@@ -13965,6 +13993,19 @@ export function createRoutes(ctx) {
13965
13993
  res.status(404).json({ error: 'CommitmentTracker not configured' });
13966
13994
  return;
13967
13995
  }
13996
+ // P1.5b §3.4 owner-routing (replica-targeted beacon-field PATCH forwards).
13997
+ {
13998
+ const pOrigin = typeof req.query.origin === 'string' ? req.query.origin : undefined;
13999
+ const pRoute = await resolveCommitmentRoute(req.params.id, pOrigin);
14000
+ if (pRoute === 'ambiguous') {
14001
+ res.status(409).json({ error: `Commitment id ${req.params.id} is ambiguous across machines — retry with ?origin=<machineId>` });
14002
+ return;
14003
+ }
14004
+ if (typeof pRoute === 'object') {
14005
+ await forwardMutation(res, pRoute.forward.owner, mutatePayload(req, pRoute.forward.owner, 'patch-beacon', req.body ?? {}));
14006
+ return;
14007
+ }
14008
+ }
13968
14009
  const existing = ctx.commitmentTracker.get(req.params.id);
13969
14010
  if (!existing) {
13970
14011
  res.status(404).json({ error: `Commitment ${req.params.id} not found` });
@@ -14071,11 +14112,79 @@ export function createRoutes(ctx) {
14071
14112
  * came back with the promised update," separate from `verified` which means
14072
14113
  * "config state is as promised."
14073
14114
  */
14074
- router.post('/commitments/:id/deliver', (req, res) => {
14115
+ /**
14116
+ * P1.5b §3.4 — owner-routing decision for a mutation route. Returns:
14117
+ * 'local' → the record is OWN (or the layer is dark): mutate locally.
14118
+ * {forward} → the record lives ONLY in a replica: forward to its owner.
14119
+ * 'ambiguous' → a bare id matches own AND replica rows (409, §3.1).
14120
+ * 'absent' → nowhere in the merged view (404 falls through to local).
14121
+ */
14122
+ async function resolveCommitmentRoute(id, origin) {
14123
+ if (!ctx.commitmentReplicaStore || !ctx.commitmentTracker)
14124
+ return 'local';
14125
+ const syncMod = await import('../core/CommitmentsSync.js');
14126
+ const rows = syncMod.mergeCommitmentViews({
14127
+ ownMachineId: ctx.meshSelfId ?? 'local',
14128
+ own: ctx.commitmentTracker.getAll(),
14129
+ replicas: ctx.commitmentReplicaStore.allReplicas(),
14130
+ });
14131
+ const resolved = syncMod.resolveBareId(rows, id, origin);
14132
+ if (resolved === 'ambiguous')
14133
+ return 'ambiguous';
14134
+ if (resolved === null)
14135
+ return 'absent';
14136
+ if (resolved.viewSource === 'own')
14137
+ return 'local';
14138
+ return { forward: { owner: resolved.originMachineId } };
14139
+ }
14140
+ /** Shared forward path: verdict → mapped response; queued → honest 202. */
14141
+ async function forwardMutation(res, owner, payload) {
14142
+ if (!ctx.forwardCommitmentMutate) {
14143
+ res.status(409).json({
14144
+ error: `Commitment ${payload.id} lives on ${owner} and cross-machine mutation is not enabled here`,
14145
+ });
14146
+ return;
14147
+ }
14148
+ const r = await ctx.forwardCommitmentMutate(owner, payload);
14149
+ if (r.kind === 'queued') {
14150
+ res.status(202).json({ queued: true, owner, reason: r.reason, opKey: payload.opKey });
14151
+ return;
14152
+ }
14153
+ const v = r.outcome;
14154
+ if (v.verdict === 'applied' || v.verdict === 'idempotent-noop') {
14155
+ res.json({ delivered: payload.op === 'deliver' ? true : undefined, op: payload.op, id: payload.id, owner, verdict: v.verdict, status: v.status, ...(v.staleObservation ? { staleObservation: true } : {}) });
14156
+ return;
14157
+ }
14158
+ res.status(v.verdict === 'not-found' ? 404 : 409).json({ error: `owner verdict: ${v.verdict}`, owner, status: v.status });
14159
+ }
14160
+ function mutatePayload(req, origin, op, args) {
14161
+ return {
14162
+ origin,
14163
+ id: req.params.id,
14164
+ op,
14165
+ ...(args ? { args } : {}),
14166
+ opKey: randomUUID(),
14167
+ requestedAt: new Date().toISOString(),
14168
+ callerMachineId: ctx.meshSelfId ?? 'local',
14169
+ ...(typeof req.query.observedStatus === 'string' ? { observedStatus: req.query.observedStatus } : {}),
14170
+ };
14171
+ }
14172
+ router.post('/commitments/:id/deliver', async (req, res) => {
14075
14173
  if (!ctx.commitmentTracker) {
14076
14174
  res.status(404).json({ error: 'CommitmentTracker not configured' });
14077
14175
  return;
14078
14176
  }
14177
+ const origin = typeof req.query.origin === 'string' ? req.query.origin : undefined;
14178
+ const route = await resolveCommitmentRoute(req.params.id, origin);
14179
+ if (route === 'ambiguous') {
14180
+ res.status(409).json({ error: `Commitment id ${req.params.id} is ambiguous across machines — retry with ?origin=<machineId>` });
14181
+ return;
14182
+ }
14183
+ if (typeof route === 'object') {
14184
+ const { deliveryMessageId } = req.body ?? {};
14185
+ await forwardMutation(res, route.forward.owner, mutatePayload(req, route.forward.owner, 'deliver', deliveryMessageId ? { deliveryMessageId } : undefined));
14186
+ return;
14187
+ }
14079
14188
  const { deliveryMessageId } = req.body ?? {};
14080
14189
  const updated = ctx.commitmentTracker.deliver(req.params.id, deliveryMessageId);
14081
14190
  if (!updated) {
@@ -14084,7 +14193,7 @@ export function createRoutes(ctx) {
14084
14193
  }
14085
14194
  res.json({ delivered: true, id: updated.id, commitment: updated });
14086
14195
  });
14087
- router.post('/commitments/:id/withdraw', (req, res) => {
14196
+ router.post('/commitments/:id/withdraw', async (req, res) => {
14088
14197
  if (!ctx.commitmentTracker) {
14089
14198
  res.status(404).json({ error: 'CommitmentTracker not configured' });
14090
14199
  return;
@@ -14094,6 +14203,17 @@ export function createRoutes(ctx) {
14094
14203
  res.status(400).json({ error: 'reason is required' });
14095
14204
  return;
14096
14205
  }
14206
+ // P1.5b §3.4 owner-routing (replica-targeted withdraw forwards).
14207
+ const wOrigin = typeof req.query.origin === 'string' ? req.query.origin : undefined;
14208
+ const wRoute = await resolveCommitmentRoute(req.params.id, wOrigin);
14209
+ if (wRoute === 'ambiguous') {
14210
+ res.status(409).json({ error: `Commitment id ${req.params.id} is ambiguous across machines — retry with ?origin=<machineId>` });
14211
+ return;
14212
+ }
14213
+ if (typeof wRoute === 'object') {
14214
+ await forwardMutation(res, wRoute.forward.owner, mutatePayload(req, wRoute.forward.owner, 'withdraw', { reason }));
14215
+ return;
14216
+ }
14097
14217
  const success = ctx.commitmentTracker.withdraw(req.params.id, reason);
14098
14218
  if (!success) {
14099
14219
  res.status(404).json({ error: `Commitment ${req.params.id} not found or already resolved` });
@@ -14109,11 +14229,22 @@ export function createRoutes(ctx) {
14109
14229
  * event. No-op (404) for commitments that aren't paused or are in a terminal
14110
14230
  * status.
14111
14231
  */
14112
- router.post('/commitments/:id/resume', (req, res) => {
14232
+ router.post('/commitments/:id/resume', async (req, res) => {
14113
14233
  if (!ctx.commitmentTracker) {
14114
14234
  res.status(404).json({ error: 'CommitmentTracker not configured' });
14115
14235
  return;
14116
14236
  }
14237
+ // P1.5b §3.4 owner-routing (replica-targeted resume forwards).
14238
+ const rOrigin = typeof req.query.origin === 'string' ? req.query.origin : undefined;
14239
+ const rRoute = await resolveCommitmentRoute(req.params.id, rOrigin);
14240
+ if (rRoute === 'ambiguous') {
14241
+ res.status(409).json({ error: `Commitment id ${req.params.id} is ambiguous across machines — retry with ?origin=<machineId>` });
14242
+ return;
14243
+ }
14244
+ if (typeof rRoute === 'object') {
14245
+ await forwardMutation(res, rRoute.forward.owner, mutatePayload(req, rRoute.forward.owner, 'resume'));
14246
+ return;
14247
+ }
14117
14248
  const updated = ctx.commitmentTracker.resume(req.params.id);
14118
14249
  if (!updated) {
14119
14250
  res.status(404).json({ error: `Commitment ${req.params.id} not found, not paused, or in terminal status` });