aibroker 0.41.1 → 0.43.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/dist/daemon/budget-cli.d.ts +19 -0
- package/dist/daemon/budget-cli.d.ts.map +1 -0
- package/dist/daemon/budget-cli.js +118 -0
- package/dist/daemon/budget-cli.js.map +1 -0
- package/dist/daemon/cli.js +5 -0
- package/dist/daemon/cli.js.map +1 -1
- package/dist/daemon/inbound-cli.d.ts.map +1 -1
- package/dist/daemon/inbound-cli.js +23 -1
- package/dist/daemon/inbound-cli.js.map +1 -1
- package/dist/daemon/inbound.d.ts +10 -0
- package/dist/daemon/inbound.d.ts.map +1 -1
- package/dist/daemon/inbound.js +54 -0
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/manage.d.ts +54 -1
- package/dist/daemon/manage.d.ts.map +1 -1
- package/dist/daemon/manage.js +280 -11
- package/dist/daemon/manage.js.map +1 -1
- package/dist/daemon/pointer-controls.d.ts +108 -0
- package/dist/daemon/pointer-controls.d.ts.map +1 -0
- package/dist/daemon/pointer-controls.js +175 -0
- package/dist/daemon/pointer-controls.js.map +1 -0
- package/hooks/budget-stop.mjs +65 -0
- package/package.json +1 -1
package/dist/daemon/manage.js
CHANGED
|
@@ -30,6 +30,7 @@ import { discoverLiveSessions } from "../core/session-discovery.js";
|
|
|
30
30
|
import { hasPailotClients } from "../adapters/pailot/gateway.js";
|
|
31
31
|
import { getAibpBridge } from "../core/state.js";
|
|
32
32
|
import { listDialogs, answerDialog } from "./dialogs.js";
|
|
33
|
+
import { readControls, grantUntil, returnToOperator, verdict as screenVerdict, describeControls, screenGrantedClause, } from "./pointer-controls.js";
|
|
33
34
|
const STATE_FILE = join(homedir(), ".aibroker", "managers.json");
|
|
34
35
|
/** How often every manager is looked at. Cheap: one content read per managed session. */
|
|
35
36
|
const TICK_MS = 20_000;
|
|
@@ -638,9 +639,30 @@ export function resolveSession(idOrName) {
|
|
|
638
639
|
if (byId)
|
|
639
640
|
return { sessionId: byId.id, name: byId.paiName ?? byId.name ?? id };
|
|
640
641
|
const needle = idOrName.toLowerCase();
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
642
|
+
const exact = (s) => (s.paiName ?? "").toLowerCase() === needle;
|
|
643
|
+
const loose = (s) => (s.name ?? "").toLowerCase().includes(needle);
|
|
644
|
+
/*
|
|
645
|
+
* A SESSION ALREADY UNDER MANAGEMENT WINS.
|
|
646
|
+
*
|
|
647
|
+
* The loose match is on the pane title, and pane titles are not unique: a
|
|
648
|
+
* plain shell opened in the project directory carries the project's name too.
|
|
649
|
+
* One appeared beside a managed session and was matched first — `status`
|
|
650
|
+
* answered "not being managed", which is true of that pane and says nothing
|
|
651
|
+
* about the session anybody meant. The next step would have been worse:
|
|
652
|
+
* arming types a goal at whatever was resolved, and a goal typed at a shell
|
|
653
|
+
* prompt runs as shell commands.
|
|
654
|
+
*
|
|
655
|
+
* Being managed is the strongest evidence available about which pane was
|
|
656
|
+
* meant, because somebody said so explicitly, by id, at some point.
|
|
657
|
+
*/
|
|
658
|
+
const managed = new Set(Object.keys(loadState()));
|
|
659
|
+
const candidates = live.filter((s) => exact(s) || loose(s));
|
|
660
|
+
const pick = candidates.find((s) => managed.has(s.id) && exact(s)) ??
|
|
661
|
+
candidates.find((s) => managed.has(s.id)) ??
|
|
662
|
+
candidates.find(exact) ??
|
|
663
|
+
candidates[0];
|
|
664
|
+
if (pick)
|
|
665
|
+
return { sessionId: pick.id, name: pick.paiName ?? pick.name ?? idOrName };
|
|
644
666
|
return null;
|
|
645
667
|
}
|
|
646
668
|
/**
|
|
@@ -1007,6 +1029,39 @@ export function writeStandingRules(text, path = RULES_FILE) {
|
|
|
1007
1029
|
* by a sentence does not walk back over it.
|
|
1008
1030
|
*/
|
|
1009
1031
|
const GOAL_MAX_CHARS = 3800;
|
|
1032
|
+
/**
|
|
1033
|
+
* `until 08:00` — an END, rather than a length.
|
|
1034
|
+
*
|
|
1035
|
+
* The two are not interchangeable in use, even though either can be converted
|
|
1036
|
+
* into the other. A person deciding at midnight says "until eight" because
|
|
1037
|
+
* eight is when they will be back; working out that this is seven and three
|
|
1038
|
+
* quarter hours is arithmetic done at the exact moment somebody is too tired to
|
|
1039
|
+
* do it, and a slip lands as a permission that ends in the dark.
|
|
1040
|
+
*
|
|
1041
|
+
* Reads the NEXT time the clock shows that hour, so "until 08:00" said at
|
|
1042
|
+
* midnight means this morning and said at nine means tomorrow. Returns null
|
|
1043
|
+
* when no such phrase is there, which is most of the time.
|
|
1044
|
+
*/
|
|
1045
|
+
export function parseUntilClock(text, now = new Date()) {
|
|
1046
|
+
const m = text.match(/\b(?:until|till|til)\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i);
|
|
1047
|
+
if (!m)
|
|
1048
|
+
return null;
|
|
1049
|
+
let hour = Number(m[1]);
|
|
1050
|
+
const minute = m[2] ? Number(m[2]) : 0;
|
|
1051
|
+
const half = (m[3] ?? "").toLowerCase();
|
|
1052
|
+
if (hour > 23 || minute > 59)
|
|
1053
|
+
return null;
|
|
1054
|
+
if (half === "pm" && hour < 12)
|
|
1055
|
+
hour += 12;
|
|
1056
|
+
if (half === "am" && hour === 12)
|
|
1057
|
+
hour = 0;
|
|
1058
|
+
const target = new Date(now);
|
|
1059
|
+
target.setHours(hour, minute, 0, 0);
|
|
1060
|
+
// Already gone today means they mean tomorrow — the usual case for a night.
|
|
1061
|
+
if (target.getTime() <= now.getTime())
|
|
1062
|
+
target.setDate(target.getDate() + 1);
|
|
1063
|
+
return target.getTime();
|
|
1064
|
+
}
|
|
1010
1065
|
/** Sensible default when a shift names no length. Long enough to be worth arming. */
|
|
1011
1066
|
const SHIFT_DEFAULT_HOURS = 8;
|
|
1012
1067
|
/** Nobody should be able to hand over a machine for longer than a day by accident. */
|
|
@@ -1019,11 +1074,15 @@ const SHIFT_MAX_HOURS = 24;
|
|
|
1019
1074
|
* controls, two workers at most" into an action, and the failure to avoid is a
|
|
1020
1075
|
* shift that silently lasts a different length than the one that was said.
|
|
1021
1076
|
*/
|
|
1022
|
-
export function parseShift(text) {
|
|
1077
|
+
export function parseShift(text, now = new Date()) {
|
|
1023
1078
|
const t = text.toLowerCase();
|
|
1079
|
+
// An end time beats a length, because somebody who said both meant the end.
|
|
1080
|
+
const endsAt = parseUntilClock(t, now);
|
|
1024
1081
|
const h = t.match(/(\d+(?:[.,]\d+)?)\s*(h\b|hours?|hrs?)/);
|
|
1025
1082
|
const m = t.match(/(\d+)\s*(m\b|minutes?|mins?)/);
|
|
1026
|
-
const hours =
|
|
1083
|
+
const hours = endsAt !== null
|
|
1084
|
+
? (endsAt - now.getTime()) / 3_600_000
|
|
1085
|
+
: h ? Number(h[1].replace(",", ".")) : m ? Number(m[1]) / 60 : SHIFT_DEFAULT_HOURS;
|
|
1027
1086
|
// The screen is withheld unless it was actually offered. A shift that grants
|
|
1028
1087
|
// the pointer because nobody said not to is the wrong way round.
|
|
1029
1088
|
const visual = /\b(your controls|with controls|visual|you have the screen|screen is yours)\b/.test(t)
|
|
@@ -1095,6 +1154,57 @@ export function composeGoal(objective, rules, hands, extra, rulesPath) {
|
|
|
1095
1154
|
: "";
|
|
1096
1155
|
return oneLine(`/goal ${objective}${pointer}${hands}${extra}`);
|
|
1097
1156
|
}
|
|
1157
|
+
/**
|
|
1158
|
+
* The screen grant the manager is currently holding open, if any.
|
|
1159
|
+
*
|
|
1160
|
+
* One place, because the operator can hand the screen over two ways — a shift,
|
|
1161
|
+
* or `hands on for eight hours` — and a rule that only knew about one of them
|
|
1162
|
+
* would silently not apply to the other. Both are the same fact: the screen is
|
|
1163
|
+
* the session's until a stated moment.
|
|
1164
|
+
*/
|
|
1165
|
+
export function screenLease(m, now) {
|
|
1166
|
+
if (m.noScreen)
|
|
1167
|
+
return null;
|
|
1168
|
+
const until = m.shift?.visual && m.shift.until > now
|
|
1169
|
+
? m.shift.until
|
|
1170
|
+
: m.handsUntil && m.handsUntil > now
|
|
1171
|
+
? m.handsUntil
|
|
1172
|
+
: null;
|
|
1173
|
+
if (until === null)
|
|
1174
|
+
return null;
|
|
1175
|
+
return { until, since: m.screenSince ?? m.shift?.startedAt ?? m.startedAt };
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* The screen, in one line a watcher can act on.
|
|
1179
|
+
*
|
|
1180
|
+
* TWO facts, deliberately in one place, because the failure they explain is
|
|
1181
|
+
* their disagreement: the manager can be telling the session "do no pointer
|
|
1182
|
+
* work" in every arming while the pointer tool would happily allow a click, or
|
|
1183
|
+
* the reverse. Reporting only one of them makes the other invisible, and an
|
|
1184
|
+
* invisible disagreement is how a night of visual work was lost without
|
|
1185
|
+
* anything anywhere looking wrong.
|
|
1186
|
+
*/
|
|
1187
|
+
function screenStatus(m, now = Date.now()) {
|
|
1188
|
+
const lease = screenLease(m, now);
|
|
1189
|
+
const tool = readControls(undefined, now);
|
|
1190
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
1191
|
+
const at = (ms) => {
|
|
1192
|
+
const d = new Date(ms);
|
|
1193
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
1194
|
+
};
|
|
1195
|
+
if (!lease) {
|
|
1196
|
+
const stance = m.noScreen
|
|
1197
|
+
? "withheld by the manager — every arming says do no pointer work"
|
|
1198
|
+
: "not granted for any period — the session may use it, nothing is holding it open";
|
|
1199
|
+
return tool.effective === "agent"
|
|
1200
|
+
? `screen: ${stance}; the pointer tool would still allow a click`
|
|
1201
|
+
: `screen: ${stance}`;
|
|
1202
|
+
}
|
|
1203
|
+
if (tool.effective === "agent") {
|
|
1204
|
+
return `screen: the session has it until ${at(lease.until)}, and the pointer tool agrees`;
|
|
1205
|
+
}
|
|
1206
|
+
return `screen: granted until ${at(lease.until)}, but ${describeControls(tool, now).replace(/^screen: /, "")} — renewing`;
|
|
1207
|
+
}
|
|
1098
1208
|
/** The text actually typed at the session. Short goal, context by reference. */
|
|
1099
1209
|
function goalText(m) {
|
|
1100
1210
|
const extra = m.pending.length ? ` OPERATOR, since you were last armed: ${m.pending.join(" ")}` : "";
|
|
@@ -1102,9 +1212,19 @@ function goalText(m) {
|
|
|
1102
1212
|
// lasts only until the session next reads a goal — and the goal is what tells
|
|
1103
1213
|
// it what to do. So a standing rule that is not in the goal is a rule with a
|
|
1104
1214
|
// lifetime of one turn, and the next arming would send it back to clicking.
|
|
1215
|
+
//
|
|
1216
|
+
// The positive case has to ride along too, and for a sharper reason. Silence
|
|
1217
|
+
// about the screen is not neutral: a session that has been careful with the
|
|
1218
|
+
// operator's machine all night reads it as permission having quietly ended,
|
|
1219
|
+
// and one of them announced it had handed the controls back while the grant
|
|
1220
|
+
// on disk still had two hours on it. Saying the expiry out loud, every time,
|
|
1221
|
+
// is what stops a session inventing one.
|
|
1105
1222
|
const hands = m.noScreen
|
|
1106
1223
|
? " THE OPERATOR HAS THE SCREEN: do no screen or pointer work at all, and do not ask for it. Everything else continues as normal. Where something would need checking on screen, write down what would need checking instead of checking it."
|
|
1107
|
-
:
|
|
1224
|
+
: (() => {
|
|
1225
|
+
const lease = screenLease(m, Date.now());
|
|
1226
|
+
return lease ? screenGrantedClause(lease.until) : "";
|
|
1227
|
+
})();
|
|
1108
1228
|
return composeGoal(m.objective, readStandingRules(), hands, extra, standingRulesSource());
|
|
1109
1229
|
}
|
|
1110
1230
|
/**
|
|
@@ -1359,14 +1479,119 @@ async function tick() {
|
|
|
1359
1479
|
if (m.shift && now >= m.shift.until && !m.shift.endReported) {
|
|
1360
1480
|
m.shift.endReported = true;
|
|
1361
1481
|
m.paused = true;
|
|
1482
|
+
// A grant must never outlive the shift it was made for. Handing it back
|
|
1483
|
+
// here means the operator finds their own machine in the morning without
|
|
1484
|
+
// having to remember which of the night's decisions is still in force.
|
|
1485
|
+
if (m.shift.visual) {
|
|
1486
|
+
try {
|
|
1487
|
+
returnToOperator("shift ended — grant returned by the manager");
|
|
1488
|
+
delete m.screenSince;
|
|
1489
|
+
}
|
|
1490
|
+
catch (e) {
|
|
1491
|
+
log(`[manage:${m.name}] could not return the screen — ${e.message}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1362
1494
|
notify(m, `shift over after ${Math.round((now - m.shift.startedAt) / 3_600_000)}h — no longer arming; the objective is kept, \`resume\` continues it`);
|
|
1363
1495
|
dirty = true;
|
|
1364
1496
|
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Keep the screen for as long as the shift was given.
|
|
1499
|
+
*
|
|
1500
|
+
* The grant is an idle window, refreshed by each pointer call, so a session
|
|
1501
|
+
* that spends two hours reading code and running tests loses the screen for
|
|
1502
|
+
* having worked — and finds out only when it next needs to look at
|
|
1503
|
+
* something. Renewing here is the arming rule applied to the other half of
|
|
1504
|
+
* what was handed over: a standing decision has to be restated to survive,
|
|
1505
|
+
* whether it is the objective or the controls.
|
|
1506
|
+
*
|
|
1507
|
+
* The operator always wins. An operator hold taken AFTER the shift began is
|
|
1508
|
+
* a deliberate act and is left alone — reported once, then obeyed.
|
|
1509
|
+
*/
|
|
1510
|
+
const lease = screenLease(m, now);
|
|
1511
|
+
if (lease) {
|
|
1512
|
+
try {
|
|
1513
|
+
const controls = readControls();
|
|
1514
|
+
const v = screenVerdict(controls, lease.since, lease.until, now);
|
|
1515
|
+
if (v.action === "renew") {
|
|
1516
|
+
grantUntil(lease.until, `held until ${new Date(lease.until).toISOString()} by the manager`);
|
|
1517
|
+
note(m, `screen grant renewed — ${v.why}`);
|
|
1518
|
+
dirty = true;
|
|
1519
|
+
}
|
|
1520
|
+
else if (v.action === "stand-off" && !m.screenStandOffReported) {
|
|
1521
|
+
m.screenStandOffReported = true;
|
|
1522
|
+
/*
|
|
1523
|
+
* "The operator" is the safe reading, not a proven one.
|
|
1524
|
+
*
|
|
1525
|
+
* The pointer tool's file records only that somebody wrote an
|
|
1526
|
+
* operator hold; a session standing itself down writes exactly the
|
|
1527
|
+
* same thing. Leaving the screen alone is right either way — it is
|
|
1528
|
+
* the operator's machine — but reporting it as their doing, when the
|
|
1529
|
+
* lease still had time on it, hides the case that has already cost a
|
|
1530
|
+
* night: a session deciding on its own that its time was up. So the
|
|
1531
|
+
* message says how much was left and lets a person judge.
|
|
1532
|
+
*/
|
|
1533
|
+
const left = Math.max(0, Math.round((lease.until - now) / 60_000));
|
|
1534
|
+
notify(m, `the screen went back to the operator with ${left} min still on the grant — leaving it with them; visual work stops, everything else carries on. If that was not you, the session stood itself down: \`hands on until HH:MM\` puts it back`);
|
|
1535
|
+
m.noScreen = true;
|
|
1536
|
+
m.lastRearmAt = 0;
|
|
1537
|
+
dirty = true;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
catch (e) {
|
|
1541
|
+
log(`[manage:${m.name}] screen check failed — ${e.message}`);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
else if (m.noScreen) {
|
|
1545
|
+
/*
|
|
1546
|
+
* The mirror case: the manager is withholding while the pointer tool
|
|
1547
|
+
* would allow a click. Somebody handed the screen over without the
|
|
1548
|
+
* manager hearing about it, and the next arming will countermand them —
|
|
1549
|
+
* that is exactly how a night of visual work was lost, and it stayed
|
|
1550
|
+
* invisible because nothing compared the two.
|
|
1551
|
+
*/
|
|
1552
|
+
try {
|
|
1553
|
+
const tool = readControls(undefined, now);
|
|
1554
|
+
if (tool.effective === "agent" && !m.screenDisagreementReported) {
|
|
1555
|
+
m.screenDisagreementReported = true;
|
|
1556
|
+
const ends = new Date(tool.until).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1557
|
+
notify(m, `the screen was handed over outside the manager (grant runs to ${ends}) — I am still telling it to do no pointer work, so the two disagree. \`hands on until ${ends}\` makes it stick`);
|
|
1558
|
+
dirty = true;
|
|
1559
|
+
}
|
|
1560
|
+
else if (tool.effective !== "agent" && m.screenDisagreementReported) {
|
|
1561
|
+
delete m.screenDisagreementReported;
|
|
1562
|
+
dirty = true;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
catch (e) {
|
|
1566
|
+
log(`[manage:${m.name}] screen check failed — ${e.message}`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
/*
|
|
1570
|
+
* A shift outranks a timed hands decision that expires inside it.
|
|
1571
|
+
*
|
|
1572
|
+
* The night this was written for: a screen grant made with `hands on for
|
|
1573
|
+
* N hours` ran out while the work it was made for was still going. The
|
|
1574
|
+
* manager did exactly as told — typed "my controls", the session stopped
|
|
1575
|
+
* all visual work and announced it had handed the controls back — and
|
|
1576
|
+
* nothing gave them back, because a timed decision knows only its own
|
|
1577
|
+
* clock. Hours of visual work were lost to a permission that expired
|
|
1578
|
+
* while the permission it was part of was still running.
|
|
1579
|
+
*/
|
|
1580
|
+
if (m.handsUntil && now >= m.handsUntil && m.shift?.visual && now < m.shift.until) {
|
|
1581
|
+
m.handsUntil = m.shift.until;
|
|
1582
|
+
m.handsWas = false;
|
|
1583
|
+
note(m, "timed screen grant extended to the end of the shift, which is still running");
|
|
1584
|
+
dirty = true;
|
|
1585
|
+
}
|
|
1365
1586
|
if (m.handsUntil && now >= m.handsUntil) {
|
|
1366
1587
|
const wasOff = m.handsWas === true;
|
|
1367
1588
|
delete m.handsUntil;
|
|
1368
1589
|
delete m.handsWas;
|
|
1369
1590
|
m.noScreen = !wasOff;
|
|
1591
|
+
if (m.noScreen)
|
|
1592
|
+
delete m.screenSince;
|
|
1593
|
+
else
|
|
1594
|
+
m.screenSince = now;
|
|
1370
1595
|
typeIntoSession(m.sessionId, m.noScreen
|
|
1371
1596
|
? "The time you had the screen for is up — my controls. The operator may be back at the machine, so stop screen and pointer work now, write down how far you got and what still needs checking on screen, and carry on with everything that does not need it."
|
|
1372
1597
|
: "your controls. The screen is yours again — the operator's hold has expired. You may resume visual work where your notes left it.");
|
|
@@ -1712,9 +1937,11 @@ export async function handleManage(sessionIdOrName, rawArg) {
|
|
|
1712
1937
|
` hands off the operator needs the screen: stops visual work at once,\n` +
|
|
1713
1938
|
` keeps everything else going, and says why\n` +
|
|
1714
1939
|
` hands on give the screen back\n` +
|
|
1715
|
-
` hands on|off for 8 hours | 30m\n` +
|
|
1940
|
+
` hands on|off for 8 hours | 30m | until 08:00\n` +
|
|
1716
1941
|
` same, but it reverts by itself — a permission that ends\n` +
|
|
1717
|
-
` only when somebody remembers outlives its reason
|
|
1942
|
+
` only when somebody remembers outlives its reason.\n` +
|
|
1943
|
+
` \`until\` takes the next time the clock reads it, which is\n` +
|
|
1944
|
+
` what somebody deciding at midnight actually means\n` +
|
|
1718
1945
|
` handover <path> [clear]\n` +
|
|
1719
1946
|
` where this session writes what it knows. At 82% context it\n` +
|
|
1720
1947
|
` is asked to update that file, then carries on — the terminal\n` +
|
|
@@ -1728,7 +1955,7 @@ export async function handleManage(sessionIdOrName, rawArg) {
|
|
|
1728
1955
|
` HOW to work, as opposed to what to work on: typed into every\n` +
|
|
1729
1956
|
` arming of every managed session, so it is written once rather\n` +
|
|
1730
1957
|
` than into each objective by hand. No argument shows them\n` +
|
|
1731
|
-
` shift [8h] [your controls] [2 workers]\n` +
|
|
1958
|
+
` shift [8h | until 08:00] [your controls] [2 workers]\n` +
|
|
1732
1959
|
` a bounded stretch of autonomous work on the tracker's open\n` +
|
|
1733
1960
|
` issues: objective, screen decision, expiry and arming in one\n` +
|
|
1734
1961
|
` sentence. \`shift off\` stands it down without killing it\n` +
|
|
@@ -1843,6 +2070,25 @@ export async function handleManage(sessionIdOrName, rawArg) {
|
|
|
1843
2070
|
existing.handsUntil = until;
|
|
1844
2071
|
existing.handsWas = !visual;
|
|
1845
2072
|
existing.shift = { until, workers, visual, startedAt: Date.now() };
|
|
2073
|
+
if (visual)
|
|
2074
|
+
existing.screenSince = Date.now();
|
|
2075
|
+
else
|
|
2076
|
+
delete existing.screenSince;
|
|
2077
|
+
delete existing.screenStandOffReported;
|
|
2078
|
+
delete existing.screenDisagreementReported;
|
|
2079
|
+
// Record the grant now rather than waiting for the session to ask for it.
|
|
2080
|
+
// The operator said the screen was its own for these hours; the pointer
|
|
2081
|
+
// tool is where that has to be written down for a click to be allowed.
|
|
2082
|
+
let screenLine = "";
|
|
2083
|
+
if (visual) {
|
|
2084
|
+
try {
|
|
2085
|
+
grantUntil(until, `held for a shift ending ${new Date(until).toISOString()}`);
|
|
2086
|
+
screenLine = ` Screen granted and held until ${new Date(until).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} — renewed on every tick, so it cannot lapse while working.\n`;
|
|
2087
|
+
}
|
|
2088
|
+
catch (e) {
|
|
2089
|
+
screenLine = ` Could not record the screen grant (${e.message}) — pointer work may be refused.\n`;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
1846
2092
|
// Arm on the next tick rather than typing over whatever is on screen now.
|
|
1847
2093
|
existing.lastRearmAt = 0;
|
|
1848
2094
|
note(existing, `shift started: ${hours}h, ${visual ? "screen granted" : "no screen"}, up to ${workers} worker(s)`);
|
|
@@ -1853,6 +2099,7 @@ export async function handleManage(sessionIdOrName, rawArg) {
|
|
|
1853
2099
|
managed: true,
|
|
1854
2100
|
message: `${name} is on shift for ${hours} hour(s), until ${ends}.\n` +
|
|
1855
2101
|
` ${visual ? "The screen is its own until then, and reverts by itself." : "No screen work — the operator has the machine."}\n` +
|
|
2102
|
+
screenLine +
|
|
1856
2103
|
` Objective set to the tracker's open issues; the standing rules ride along with every arming.\n` +
|
|
1857
2104
|
(workers > 1
|
|
1858
2105
|
? ` Asked for ${workers} workers. Only one runs today — worktrees and the claim protocol are designed but not built, and a second worker without them would share a checkout with the first.\n`
|
|
@@ -1897,6 +2144,10 @@ export async function handleManage(sessionIdOrName, rawArg) {
|
|
|
1897
2144
|
`objective: ${existing.objective}\n` +
|
|
1898
2145
|
`\nright now:\n` +
|
|
1899
2146
|
liveReading(sessionId, idle) +
|
|
2147
|
+
// Who holds the screen belongs in the live reading, not in the
|
|
2148
|
+
// manager's own record: it is a fact about the machine right now, and
|
|
2149
|
+
// it is the one a watcher cannot get any other way.
|
|
2150
|
+
`\n ${screenStatus(existing)}` +
|
|
1900
2151
|
`\n\nthe manager: ${armed}` +
|
|
1901
2152
|
(existing.pending.length ? `, ${existing.pending.length} instruction(s) waiting to go out` : "") +
|
|
1902
2153
|
(last ? `\n${last}` : ""),
|
|
@@ -1996,8 +2247,16 @@ export async function handleManage(sessionIdOrName, rawArg) {
|
|
|
1996
2247
|
* Both matter for the same reason: a permission that only ends when a person
|
|
1997
2248
|
* remembers to end it is a permission that outlives its reason.
|
|
1998
2249
|
*/
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2250
|
+
const said = handsMatch?.[2] ?? "";
|
|
2251
|
+
const untilClock = parseUntilClock(said);
|
|
2252
|
+
const dur = untilClock === null
|
|
2253
|
+
? said.match(/(?:for\s+)?(\d+(?:\.\d+)?)\s*(h|hr|hrs|hour|hours|m|min|mins|minute|minutes)\b/i)
|
|
2254
|
+
: null;
|
|
2255
|
+
if (untilClock !== null) {
|
|
2256
|
+
existing.handsUntil = untilClock;
|
|
2257
|
+
existing.handsWas = off;
|
|
2258
|
+
}
|
|
2259
|
+
else if (dur) {
|
|
2001
2260
|
const n = Number(dur[1]);
|
|
2002
2261
|
const unit = dur[2].toLowerCase();
|
|
2003
2262
|
const ms = /^h/.test(unit) ? n * 3_600_000 : n * 60_000;
|
|
@@ -2009,6 +2268,16 @@ export async function handleManage(sessionIdOrName, rawArg) {
|
|
|
2009
2268
|
delete existing.handsWas;
|
|
2010
2269
|
}
|
|
2011
2270
|
existing.noScreen = off;
|
|
2271
|
+
// A grant that starts now: recorded so a later operator hold can be told
|
|
2272
|
+
// apart from this one having lapsed. Handing the screen back clears it.
|
|
2273
|
+
if (off) {
|
|
2274
|
+
delete existing.screenSince;
|
|
2275
|
+
}
|
|
2276
|
+
else {
|
|
2277
|
+
existing.screenSince = Date.now();
|
|
2278
|
+
delete existing.screenStandOffReported;
|
|
2279
|
+
delete existing.screenDisagreementReported;
|
|
2280
|
+
}
|
|
2012
2281
|
if (off) {
|
|
2013
2282
|
// The reason comes FIRST, and the phrase is inside a sentence rather than
|
|
2014
2283
|
// barked on its own. "my controls" alone revokes the tool and explains
|