@ngockhoale/ukit 2.2.9 → 2.2.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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UKit are documented here.
|
|
4
4
|
|
|
5
|
+
## 2.2.11 - 2026-08-29
|
|
6
|
+
|
|
7
|
+
Same-day follow-up to 2.2.10. The two premises that release shipped as unverified were checked
|
|
8
|
+
against omp's own installed TypeScript source (`@oh-my-pi/pi-coding-agent` v18.0.10, not a live
|
|
9
|
+
session — omp still was not run) and both hold:
|
|
10
|
+
|
|
11
|
+
- **`steer` after a compaction is accepted, not dropped.** `session-maintenance.ts` fires
|
|
12
|
+
`session_compact` only after `agent.replaceMessages()`/`rebaseAfterCompaction()` already ran, so
|
|
13
|
+
the session is idle at that point, and `agent-session.ts`'s `sendCustomMessage()` non-streaming
|
|
14
|
+
branch appends the message to the live context regardless of `deliverAs`.
|
|
15
|
+
- **`display: true` does trigger a visible re-render.** `extension-ui-controller.ts`'s
|
|
16
|
+
`applyCustomMessageDisplay()` calls `rebuildChatFromMessages()` for the non-streaming case (the
|
|
17
|
+
streaming case renders via the `message_end` event instead).
|
|
18
|
+
|
|
19
|
+
`templates/.omp/hooks/pre/ukit-bridge.js`'s two `TODO(verify)` comments (PLAN.md §3 D3, D4) are
|
|
20
|
+
updated to record this — from "unverified, additive, worst-case-unchanged" to "source-confirmed,
|
|
21
|
+
one rung below a live repro." No runtime behaviour changes; both delivery paths (`steer` +
|
|
22
|
+
`nextTurn`, and the `finalNotice` fallback) stay in place as defense in depth either way. A stale
|
|
23
|
+
local `.omp/hooks/pre/ukit-bridge.js` install artifact in this repo — untracked, regenerated by
|
|
24
|
+
`ukit install`, and out of sync with `templates/.omp/` since before 2.2.10 shipped — was also found
|
|
25
|
+
and resynced; that file is never part of the published package.
|
|
26
|
+
|
|
27
|
+
## 2.2.10 - 2026-08-29
|
|
28
|
+
|
|
29
|
+
User report: long omp (Oh My Pi) sessions go idle roughly 15 minutes into a task — no output, no
|
|
30
|
+
error, no notification, indistinguishable from a hang. Typing `continue` makes the session resume
|
|
31
|
+
and finish correctly (the user's observation, not a measurement — no field reproduction of the
|
|
32
|
+
stall exists on this machine to time it). Three real root causes were found by reading the source,
|
|
33
|
+
not guessed:
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **`map-impact` — the mode wide implementation work actually routes to — was never gated by the
|
|
38
|
+
completion loop.** `templates/.claude/ukit/runtime/execution-ledger.mjs:12`'s `IMPLEMENT_MODES`
|
|
39
|
+
set omitted it, while `src/index/taskRouting.js:414` routes exactly this kind of task to
|
|
40
|
+
`map-impact` and `src/index/taskRouting.js:610` gives it the strictest contract
|
|
41
|
+
(`impact-evidence`, `write-evidence`, `verification-evidence`). So the longest tasks declared the
|
|
42
|
+
most evidence and got no gate at all — the session could stop with nothing written and nothing
|
|
43
|
+
verified, silently. `map-impact` is now in `IMPLEMENT_MODES`.
|
|
44
|
+
- **Reaching the continuation cap ended the session with a log line, not a message.** At
|
|
45
|
+
`MAX_CONTINUATIONS` (6, unchanged), `evaluateCompletion` returned `{ capped: true }` and the only
|
|
46
|
+
reaction was `pi.logger.warn` — a log sink, not the transcript. This is also the mechanical reason
|
|
47
|
+
a new prompt (`continue`) appeared to "fix" the stall: a new prompt writes a fresh `requestKey`,
|
|
48
|
+
which resets the per-request continuation count back to zero. The gate now spends exactly one
|
|
49
|
+
final continuation (`finalNotice`) instructing the model to state what is unfinished and stop, and
|
|
50
|
+
persists a one-shot `notified` flag on the ledger so the notice cannot refire.
|
|
51
|
+
- **The post-compact resume was delivered only to the next user turn, and every UKit→omp message was
|
|
52
|
+
`display: false`.** After an omp auto-compaction, the `docs/AI_HANDOFF/RUN.md` resume cursor was
|
|
53
|
+
sent with `deliverAs: 'nextTurn'` only — if compaction ended the agent loop, the cursor waited for
|
|
54
|
+
a human to type something. Separately, every message the bridge built hardcoded `display: false`,
|
|
55
|
+
so UKit had no user-visible channel in omp at all, for context or for stop notices.
|
|
56
|
+
`runSessionCompact` now sends the resume as `steer` (reaches a still-running turn) *and*
|
|
57
|
+
`nextTurn` (unchanged fallback), and `runSessionStop` now sends one `display: true` notice
|
|
58
|
+
whenever it stops with evidence still missing — including the previously-silent
|
|
59
|
+
`review-release` / ungated-mode case.
|
|
60
|
+
|
|
61
|
+
**Two premises behind the third fix are unverified — omp is not installed in this development
|
|
62
|
+
environment.** Whether omp still accepts a `steer` message after a compaction, and whether omp
|
|
63
|
+
honours `display: true` on a bridge message, were not and could not be reproduced or measured here.
|
|
64
|
+
Both changes are additive: if either premise is false, the `nextTurn` delivery and the model's own
|
|
65
|
+
`finalNotice` message (from the second fix) still reach the user, so the worst case is unchanged
|
|
66
|
+
from today's behaviour, never worse. See `docs/STATUS.md` for the open thread and the evidence that
|
|
67
|
+
would confirm the continuation-cap path directly.
|
|
68
|
+
|
|
69
|
+
- **`templates/.omp/RULES.md` gains one sticky rule**: ending a turn with no output is a defect, not
|
|
70
|
+
a pause — a model-side backstop for whatever a code gate cannot reach.
|
|
71
|
+
|
|
5
72
|
## 2.2.9 - 2026-08-28
|
|
6
73
|
|
|
7
74
|
Retuning UKit's context budget used to mean editing five scattered places and hand-syncing two
|
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ const IMPLEMENT_MODES = new Set([
|
|
|
15
15
|
'local-build',
|
|
16
16
|
'shared-edit',
|
|
17
17
|
'find-cause',
|
|
18
|
+
'map-impact',
|
|
18
19
|
]);
|
|
19
20
|
|
|
20
21
|
function safeSegment(value) {
|
|
@@ -159,6 +160,7 @@ function freshLedger(payload, routeState, harness) {
|
|
|
159
160
|
receipts: [],
|
|
160
161
|
blocker: null,
|
|
161
162
|
continuationCount: 0,
|
|
163
|
+
notified: false,
|
|
162
164
|
updatedAt: Date.now(),
|
|
163
165
|
};
|
|
164
166
|
}
|
|
@@ -252,24 +254,48 @@ export function evaluateCompletion({ state = {}, ledger = {} } = {}) {
|
|
|
252
254
|
const routeSummary = state?.routeSummary || {};
|
|
253
255
|
const mode = routeSummary.executionMode || routeSummary.approachSelector?.executionMode || null;
|
|
254
256
|
const evidence = requiredEvidence(state);
|
|
255
|
-
if (
|
|
256
|
-
return { continue: false, missingEvidence: [] };
|
|
257
|
+
if (ledger?.blocker) {
|
|
258
|
+
return { continue: false, notify: false, missingEvidence: [] };
|
|
259
|
+
}
|
|
260
|
+
if (evidence.length === 0) {
|
|
261
|
+
return { continue: false, notify: false, missingEvidence: [] };
|
|
257
262
|
}
|
|
258
263
|
|
|
259
264
|
const sameRequest = !ledger?.requestKey || !state?.requestKey || ledger.requestKey === state.requestKey;
|
|
260
265
|
const effectiveLedger = sameRequest ? ledger : {};
|
|
261
266
|
const missingEvidence = evidence.filter((item) => !evidenceSatisfied(item, effectiveLedger));
|
|
262
267
|
if (missingEvidence.length === 0) {
|
|
263
|
-
return { continue: false, missingEvidence: [] };
|
|
268
|
+
return { continue: false, notify: false, missingEvidence: [] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const gated = IMPLEMENT_MODES.has(mode);
|
|
272
|
+
if (!gated) {
|
|
273
|
+
return {
|
|
274
|
+
continue: false,
|
|
275
|
+
notify: true,
|
|
276
|
+
missingEvidence,
|
|
277
|
+
reason: `UKit completion gate: missing ${missingEvidence.join(', ')}. This mode does not auto-continue; tell the user what is unfinished.`,
|
|
278
|
+
};
|
|
264
279
|
}
|
|
265
280
|
|
|
266
281
|
const continuationCount = Number(effectiveLedger?.continuationCount || 0);
|
|
267
282
|
if (continuationCount >= MAX_CONTINUATIONS) {
|
|
283
|
+
if (effectiveLedger?.notified === true) {
|
|
284
|
+
return {
|
|
285
|
+
continue: false,
|
|
286
|
+
capped: true,
|
|
287
|
+
notify: true,
|
|
288
|
+
missingEvidence,
|
|
289
|
+
reason: `UKit continuation cap reached with missing evidence: ${missingEvidence.join(', ')}.`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
268
292
|
return {
|
|
269
|
-
continue:
|
|
293
|
+
continue: true,
|
|
294
|
+
finalNotice: true,
|
|
295
|
+
notify: true,
|
|
270
296
|
capped: true,
|
|
271
297
|
missingEvidence,
|
|
272
|
-
reason: `UKit
|
|
298
|
+
reason: `UKit stopping with unfinished work: ${missingEvidence.join(', ')}. Tell the user what is unfinished and stop; do not continue further.`,
|
|
273
299
|
};
|
|
274
300
|
}
|
|
275
301
|
|
|
@@ -298,6 +324,13 @@ export async function incrementContinuation(projectRoot, payload = {}, ledger =
|
|
|
298
324
|
return next;
|
|
299
325
|
}
|
|
300
326
|
|
|
327
|
+
export async function markNotified(projectRoot, payload = {}, ledger = null) {
|
|
328
|
+
const current = ledger || await readExecutionLedger(projectRoot, payload) || freshLedger(payload, null, 'unknown');
|
|
329
|
+
const next = { ...current, notified: true, updatedAt: Date.now() };
|
|
330
|
+
await writeJsonAtomic(ledgerPath(projectRoot, payload), next);
|
|
331
|
+
return next;
|
|
332
|
+
}
|
|
333
|
+
|
|
301
334
|
async function readStdin() {
|
|
302
335
|
if (process.stdin.isTTY) return '';
|
|
303
336
|
const chunks = [];
|
|
@@ -322,7 +355,8 @@ async function main() {
|
|
|
322
355
|
const ledger = await readExecutionLedger(projectRoot, payload) || {};
|
|
323
356
|
const result = evaluateCompletion({ state, ledger });
|
|
324
357
|
if (result.continue) {
|
|
325
|
-
await
|
|
358
|
+
if (result.finalNotice) await markNotified(projectRoot, payload, ledger);
|
|
359
|
+
else await incrementContinuation(projectRoot, payload, ledger);
|
|
326
360
|
process.stdout.write(`${JSON.stringify({ decision: 'block', reason: result.reason })}\n`);
|
|
327
361
|
} else if (result.capped) {
|
|
328
362
|
process.stderr.write(`[ukit-completion] ${result.reason}\n`);
|
package/templates/.omp/RULES.md
CHANGED
|
@@ -57,6 +57,12 @@ Prefer unique current-file anchors over line numbers or stale pasted blocks. Nev
|
|
|
57
57
|
stale spec — re-read current source and confirm before applying. Preserve existing BOM and line
|
|
58
58
|
endings.
|
|
59
59
|
|
|
60
|
+
## 8. Never end a turn silently
|
|
61
|
+
|
|
62
|
+
Ending a turn with no output is a defect, not a pause. If you stop before the work is finished, the
|
|
63
|
+
last thing you emit is one short line naming what is unfinished and what you need. Silence is never
|
|
64
|
+
a status — the user cannot distinguish it from a crash.
|
|
65
|
+
|
|
60
66
|
> Maintainer note, not a per-turn rule: `modelRoles` in `.omp/config.yml` ships UNIC gateway names.
|
|
61
67
|
> On a non-UNIC provider, edit only the three cost tiers — `lite`, `code`, `smart` — never `vision`,
|
|
62
68
|
> which stays `unic-vision` because it is a capability lane, not a cost tier.
|
|
@@ -12,6 +12,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
12
12
|
import {
|
|
13
13
|
evaluateCompletion,
|
|
14
14
|
incrementContinuation,
|
|
15
|
+
markNotified,
|
|
15
16
|
readExecutionLedger,
|
|
16
17
|
readRouteState,
|
|
17
18
|
recordExecutionReceipt,
|
|
@@ -337,18 +338,18 @@ function textFromContent(content) {
|
|
|
337
338
|
.join('\n');
|
|
338
339
|
}
|
|
339
340
|
|
|
340
|
-
function hookContextMessage(content) {
|
|
341
|
+
function hookContextMessage(content, { display = false } = {}) {
|
|
341
342
|
return {
|
|
342
343
|
customType: 'ukit-hook-context',
|
|
343
344
|
content,
|
|
344
|
-
display
|
|
345
|
+
display,
|
|
345
346
|
};
|
|
346
347
|
}
|
|
347
348
|
|
|
348
|
-
function sendContext(pi, context, deliverAs) {
|
|
349
|
+
function sendContext(pi, context, deliverAs, { display = false } = {}) {
|
|
349
350
|
const content = context.filter(Boolean).join('\n').trim();
|
|
350
351
|
if (!content || typeof pi.sendMessage !== 'function') return;
|
|
351
|
-
pi.sendMessage(hookContextMessage(content), { deliverAs });
|
|
352
|
+
pi.sendMessage(hookContextMessage(content, { display }), { deliverAs });
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
export async function runToolCall(pi, event, { projectRoot, context: extensionContext = {} }) {
|
|
@@ -469,6 +470,13 @@ export async function runSessionCompact(pi, event, { projectRoot, context: exten
|
|
|
469
470
|
const metadata = runtimeMetadata(event, extensionContext);
|
|
470
471
|
const payload = buildHookPayload('SessionStart', { ...metadata, source: 'compact' });
|
|
471
472
|
const result = await runScriptChain(pi, HOOK_EVENT_MAP.session_start, payload, { projectRoot });
|
|
473
|
+
// VERIFIED (2026-08-29, source read of omp v18.0.10 pi-coding-agent/src) PLAN.md §3 D3:
|
|
474
|
+
// session_compact fires only after agent.replaceMessages()/rebaseAfterCompaction() already
|
|
475
|
+
// ran (session-maintenance.ts), i.e. the session is idle here, and agent-session.ts's
|
|
476
|
+
// sendCustomMessage() non-streaming branch appends the message to the live context
|
|
477
|
+
// regardless of deliverAs — so 'steer' is accepted right after a compaction. Not a live
|
|
478
|
+
// repro, so the 'nextTurn' copy below stays as defense in depth.
|
|
479
|
+
sendContext(pi, result.context, 'steer');
|
|
472
480
|
sendContext(pi, result.context, 'nextTurn');
|
|
473
481
|
return undefined;
|
|
474
482
|
}
|
|
@@ -490,12 +498,26 @@ export async function runSessionStop(
|
|
|
490
498
|
const evaluation = evaluateCompletion({ state, ledger });
|
|
491
499
|
if (!evaluation.continue) {
|
|
492
500
|
if (evaluation.capped) pi.logger?.warn?.(`[UKit] ${evaluation.reason}`);
|
|
501
|
+
if (Array.isArray(evaluation.missingEvidence) && evaluation.missingEvidence.length > 0) {
|
|
502
|
+
const notice = [
|
|
503
|
+
`[UKit] Stopping with unfinished work: ${evaluation.missingEvidence.join(', ')}.`,
|
|
504
|
+
evaluation.reason,
|
|
505
|
+
].filter(Boolean).join(' ');
|
|
506
|
+
// VERIFIED (2026-08-29, source read of omp v18.0.10 pi-coding-agent/src) PLAN.md §3 D4:
|
|
507
|
+
// display:true on a non-streaming sendMessage payload triggers
|
|
508
|
+
// extension-ui-controller.ts's applyCustomMessageDisplay() -> rebuildChatFromMessages(),
|
|
509
|
+
// a real immediate TUI re-render (the streaming case renders via the message_end event
|
|
510
|
+
// instead). Not a live repro, so the finalNotice continuation below (the model's own
|
|
511
|
+
// message) stays as a second guaranteed channel.
|
|
512
|
+
sendContext(pi, [notice], 'nextTurn', { display: true });
|
|
513
|
+
}
|
|
493
514
|
return undefined;
|
|
494
515
|
}
|
|
495
516
|
|
|
496
517
|
if (suppliedLedger === undefined) {
|
|
497
518
|
try {
|
|
498
|
-
await
|
|
519
|
+
if (evaluation.finalNotice) await markNotified(projectRoot, payload, ledger);
|
|
520
|
+
else await incrementContinuation(projectRoot, payload, ledger);
|
|
499
521
|
} catch (error) {
|
|
500
522
|
pi.logger?.warn?.(`[UKit] continuation bookkeeping failed open: ${error?.message || error}`);
|
|
501
523
|
}
|